Credential strategies
SimplePost supports several credential strategies because each interface runs in a different environment.
Strategy matrix
| Strategy | Best for | Used by |
|---|---|---|
| Environment variables | Local development, single-account scripts, simple services | SDK, CLI fallback, self-hosted REST server |
| Explicit credentials in code | Per-user tokens stored by your app | SDK |
| Scheduler connected accounts | Users who authorize accounts in the web app | Scheduler, MCP, scheduler-connected CLI |
| Local CLI secret store | Terminal-only workflows and CI | CLI |
accounts.json | Small self-hosted HTTP server deployments | Self-hosted REST server |
Environment variables
Environment variables are the fastest way to test the SDK and examples. Each platform page lists the variables it supports. Examples:
X_CLIENT_ID=
X_REFRESH_TOKEN=
TELEGRAM_BOT_TOKEN=
YOUTUBE_CLIENT_ID=
YOUTUBE_CLIENT_SECRET=
YOUTUBE_REFRESH_TOKEN=
Notes:
- The SDK only reads env vars at the start of
post()— once you have decided to use them, do not also passoptions.<platform>.credentialsfor the same platform unless you want to override the env value. - Telegram has no
TELEGRAM_CHAT_IDenv var. The chat ID must be supplied throughoptions.telegram.chatIdon every call (or stored on a CLI /accounts.jsonaccount). - YouTube reads only
YOUTUBE_CLIENT_ID+YOUTUBE_CLIENT_SECRET+YOUTUBE_REFRESH_TOKEN. The shorteraccessToken-only variant is supported through SDK options but not through env vars. - Pinterest needs both
PINTEREST_ACCESS_TOKENandPINTEREST_BOARD_IDfor env-only setup. Without the board ID, the SDK rejects the post.
This strategy is convenient for one account per platform. It is not ideal for multi-user products.
Explicit credentials
If your app stores per-user tokens, pass credentials through platform options:
await post({
content: { text: "Hello" },
platforms: ["x"],
options: {
x: {
credentials: {
clientId: "X_APP_CLIENT_ID",
accessToken: "USER_ACCESS_TOKEN",
refreshToken: "USER_REFRESH_TOKEN",
},
},
},
});
Persist refreshed credentials returned by the SDK when the provider rotates tokens.
Scheduler connected accounts
Scheduler stores connected account secrets encrypted in the app database. This is the right strategy when users should connect accounts themselves and when MCP or the Scheduler UI should post on their behalf.
The MCP server and scheduler-connected CLI never receive raw social platform credentials. They receive SimplePost tokens that authorize access through Scheduler.
Local CLI secret store
The CLI can store credentials directly:
simplepost setup --backend keychain
simplepost account add telegram --alias announcements --bot-token "$TELEGRAM_BOT_TOKEN" --chat-id "@channel"
Use keychain on developer machines, file-encrypted for scripts and CI, and file-plain only for local testing.
accounts.json
The self-hosted REST server reads accounts from a JSON file:
{
"accounts": [
{
"id": "x-main",
"platform": "x",
"label": "Main brand X account",
"username": "yourbrand",
"platformAccountId": "1234567890",
"credentials": {
"clientId": "...",
"refreshToken": "..."
},
"options": {
"replyToId": "1234567890"
}
}
]
}
Common fields:
| Field | Required | Description |
|---|---|---|
id | Yes | Stable account ID used in API accountIds. |
platform | Yes | One of the supported platform keys. |
label | No | Human-readable account name. |
username | No | Used to build post URLs when possible. |
platformAccountId | Varies | Required by some providers such as Telegram and Facebook. |
profilePicture | No | Returned by GET /api/v1/accounts. |
credentials | Yes | Provider-specific secret values. |
options | No | Provider defaults merged into each request. |
The server validates this file at startup and refuses to boot on unknown platforms, duplicate IDs, or malformed JSON.
Token rotation
Some OAuth providers rotate refresh tokens on every refresh: the new refresh token replaces the old one, and the old one is invalidated. If you persist the wrong copy of the token, the next refresh fails and the account silently breaks.
| Platform | Rotates on refresh? | Notes |
|---|---|---|
| X | Yes, every refresh | The SDK returns refreshed credentials in result.extraData.refreshedCredentials. Persist them before the next post. |
| YouTube | No | Google refresh tokens stay valid until the user revokes them. |
| Facebook / Instagram / Threads | No (long-lived tokens are exchanged manually) | Re-issue tokens with the Access Token Debugger when they expire. |
| TikTok | Refresh tokens last ~365 days, rotate on refresh | Persist refreshed values; otherwise re-authorize. |
| No (60-day access tokens) | Re-authorize when the access token expires. | |
| Refresh tokens rotate | Persist refreshed values. | |
| Bluesky (OAuth) | Yes — DPoP-bound token rotation | Scheduler stores DPoP keys automatically. SDK consumers must persist accessToken, refreshToken, and DPoP JWKs. |
| Bluesky (app password) | n/a | App passwords do not expire on refresh. |
| Telegram | n/a | Bot tokens do not rotate. |
Where the SDK is the boundary, the SDK never writes back to your .env, accounts.json, database, or CLI store. After every post, check result.extraData?.refreshedCredentials for that platform and persist what you find:
const refreshed = results.get("x")?.extraData?.refreshedCredentials;
if (refreshed) {
await saveUserTokens(refreshed);
}
The CLI and Scheduler do persist rotated tokens automatically when posting through their stored accounts.