Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rustiz documentation

Rustiz is a headless, agent-first social-media scheduler: one Rust binary, one SQLite file and no GUI. It is a Rust port of Postiz v1.47.0, licensed AGPL-3.0. Agents drive it through MCP or the CLI; a human only opens the platform login page when OAuth requires one.

Status: the port is complete, but no provider has been tested against a real platform. All provider tests use mock servers. Expect to debug the first real connection to each platform. See Porting status for the porting tracker.

Security note: JWT_SECRET encrypts customFields credentials and custom-instance OAuth client data. Provider access tokens in the Integration.token / refreshToken columns are not encrypted in this port; they are plain text in SQLite. Encrypt the database volume and every backup, and restrict access to the database file as you would to a password store. See Self-hosting §3.2.

Documentation map

DocumentWhat it covers
Quick startBuild, init, connect an agent and post, in five minutes.
MCP setupMCP setup for Claude Desktop, Claude Code, Cursor and remote clients; the 31 tools; the posting flow.
CLIEvery CLI command, local vs remote mode, JSON output and exit codes.
REST API/public/v1 endpoints, auth, rate limits and errors.
BenchmarksThe measured numbers used on the website, and how to reproduce them.
Self-hostingInstall (local, systemd, Docker), every core environment variable, storage/S3/R2, ffmpeg, reverse proxies/TLS, OAuth callbacks, organizations/API keys, stdio vs remote MCP, backups and troubleshooting.
./providers/One page per provider: exact developer-app setup, scopes, redirect URI, customFields/is_between_steps follow-ups, post settings, media rules and implementation gotchas.
Project READMEProject overview, measured performance, CLI/MCP quick reference, rate limits.
Porting statusWhat was ported, what was removed, and known differences from Postiz.

Start in five commands

cargo build --release --locked -p rustiz
./target/release/rustiz init
./target/release/rustiz providers --status
./target/release/rustiz channels connect x --open
./target/release/rustiz serve

rustiz init prints an mcpServers block for a local agent; rustiz serve exposes the REST API at /public/v1, MCP at /mcp and the OAuth callbacks at /integrations/social/<provider>. The CLI works without a server when RUSTIZ_URL is unset, but posts only publish while rustiz serve, rustiz worker or rustiz mcp is running.

How a channel connects

connection is the value integrationSchema and GET /public/v1/providers report. It tells an agent what a human must do.

ModeMeaningExamples
oauthThe server returns a login URL; a human authorizes; the platform redirects to FRONTEND_URL/integrations/social/<identifier>.X, LinkedIn, Reddit, YouTube, Mastodon
customFieldsThe agent passes a JSON credential object; no browser and no server-side app. Credentials are encrypted at rest with JWT_SECRET.Bluesky, Medium, WordPress, Lemmy, Nostr
externalUrlThe app is registered on the user’s self-hosted instance; the server calls the instance’s /api/v1/apps and stores the returned client.mastodon-custom
telegramBotA human sends /connect <word> to the deployment’s bot, or the agent supplies the numeric chatId.Telegram
moltbookClaimThe server registers an agent; a human opens the claim URL; the agent polls.Moltbook
signerThe server creates a Neynar signer; a human approves it in the Farcaster app; the agent polls.Farcaster (wrapcast)

OAuth callbacks always use:

{FRONTEND_URL}/integrations/social/{identifier}

FRONTEND_URL has any trailing slash removed. Exceptions are called out in the master table: mastodon-custom reuses the mastodon path, tiktok-business requires a trailing slash, and x can use X_URL as its callback base.

When FRONTEND_URL does not contain the substring https (a plain-http local install), threads, slack, vk, tiktok, tiktok-business and instagram-standalone wrap the callback as https://redirectmeto.com/<FRONTEND_URL>/integrations/social/... because those platforms refuse an http:// redirect. In production, use HTTPS and the wrapper disappears.

Provider setup tiers

The tiers describe how much platform-side work stands between a fresh install and a working channel. They are about setup, not about how many features a provider supports; per-provider capabilities are on each provider page.

Tier 0 — zero developer app

Nothing to register on the platform and no server environment variable: the credentials come from the channel dialog (customFields), the instance registers the OAuth app itself (externalUrl), or the flow is a claim (moltbookClaim).

ProviderIdentifierWhat the user supplies
BlueskyblueskyService URL (default https://bsky.social), handle, app password
Dev.todevtoAPI key
HashnodehashnodeAPI key
LemmylemmyInstance URL, username, password
ListMonklistmonkURL, username, password
M. Instancemastodon-customInstance URL; the server registers an OAuth app on it
MediummediumAPI key
MoltbookmoltbookNothing; the server registers an agent and a human claims it
Nostrnostr64-character hex private key (nsec is not accepted)
WordPresswordpressSite URL, username, application password

Tier 1 — instant self-serve app or bot

You create an app or bot in a self-service portal, paste a client id/secret (or bot token), register the redirect URI and connect immediately. No platform review is needed for a private/self-hosted app; Slack and Discord only review apps that are publicly distributed.

ProviderIdentifierWhat you create
DiscorddiscordApplication + bot; client id/secret and bot token
KickkickDeveloper app (OAuth 2.1 + PKCE)
MastodonmastodonOAuth app on the instance configured in MASTODON_URL (POST /api/v1/apps); use mastodon-custom for arbitrary instances
SlackslackSlack app with bot scopes and an icon
TelegramtelegramBot from BotFather
TumblrtumblrOAuth app
TwitchtwitchDeveloper console app
VKvkVK ID app
WhopwhopOAuth app (public client, PKCE)

Tier 2 — sandbox/testing mode first, production review or verification

The app works immediately for accounts you add as testers/developers, but publishing to the public — or using the full API — requires app review, business verification, an audit, or a paid plan. Budget days to weeks, not minutes.

ProviderIdentifierGate
DribbbledribbbleApplication registration/review; scoped tokens
Facebook PagefacebookMeta app in Development mode works for roles/testers; Live mode needs business verification and advanced permissions
Instagram (Facebook Business)instagramSame Meta app; professional account linked to a Facebook Page; advanced permissions
Instagram (Standalone)instagram-standaloneSeparate Instagram app; professional account; testers until review
LinkedInlinkedinProducts: Share on LinkedIn, Sign In with LinkedIn (OIDC), Advertising API; the org scopes need the Advertising API request
LinkedIn Pagelinkedin-pageSame app and scopes; must administer a company page
PinterestpinterestTrial access works for the app owner; standard access is reviewed
RedditredditWeb app is self-serve, but API access is gated by Reddit’s Responsible Builder review; commercial use is a separate contract
ThreadsthreadsMeta app + Threads API use case; tester invites until the app is live
TikToktiktokDeveloper app + Content Posting API; unaudited Direct Post is private and limited to a few users/day; audit unlocks privacy levels
XxOAuth 1.0a app works on the free tier with strict monthly write limits; production volume needs a paid API tier
YouTubeyoutubeGoogle Cloud project + OAuth consent screen; test users work immediately, public use needs verification; brand accounts need admin trust

Tier 3 — paid, partner or special-access APIs

ProviderIdentifierGate
Google My BusinessgmbGoogle My Business API access request (days to a week) and a verified Business Profile
MeWemeweMeWe Developer Program is in beta; applications are reviewed and spots are limited
TikTok Businesstiktok-businessTikTok Business/Ads developer app and business account; separate credentials and a trailing-slash redirect URI
FarcasterwrapcastNeynar account, client id, app FID + mnemonic + secret key; Neynar plan limits and signer approval

Platform rules change frequently. If a portal does not match these notes, the portal is authoritative; the code-side contract (identifier, env var names, redirect path, scopes) is in the provider page and in the Rust source.

Master provider table

All 35 registered providers, alphabetically by identifier. — means the provider needs no server environment variable (the credentials arrive through customFields, the instance, or a claim).

Legend: * = callback is tunnelled through redirectmeto.com when FRONTEND_URL is not HTTPS. † = trailing slash is required. ‡ = shares credentials with another provider.

Display nameIdentifierConnectionRequired server env varsRedirect URI path
BlueskyblueskycustomFields—— (no browser redirect)
Dev.todevtocustomFields——
DiscorddiscordoauthDISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET (set DISCORD_BOT_TOKEN_ID too; posting uses the bot token)/integrations/social/discord
DribbbledribbbleoauthDRIBBBLE_CLIENT_ID, DRIBBBLE_CLIENT_SECRET/integrations/social/dribbble
Facebook PagefacebookoauthFACEBOOK_APP_ID, FACEBOOK_APP_SECRET ‡/integrations/social/facebook
Google My BusinessgmboauthGOOGLE_GMB_CLIENT_ID + GOOGLE_GMB_CLIENT_SECRET, or YOUTUBE_CLIENT_ID + YOUTUBE_CLIENT_SECRET ‡/integrations/social/gmb
HashnodehashnodecustomFields——
Instagram (Facebook Business)instagramoauthFACEBOOK_APP_ID, FACEBOOK_APP_SECRET ‡/integrations/social/instagram
Instagram (Standalone)instagram-standaloneoauthINSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET/integrations/social/instagram-standalone *
KickkickoauthKICK_CLIENT_ID, KICK_SECRET/integrations/social/kick
LemmylemmycustomFields——
LinkedInlinkedinoauthLINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET ‡/integrations/social/linkedin
LinkedIn Pagelinkedin-pageoauthLINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET ‡/integrations/social/linkedin-page
ListMonklistmonkcustomFields——
MastodonmastodonoauthMASTODON_CLIENT_ID, MASTODON_CLIENT_SECRET (optional MASTODON_URL, default https://mastodon.social)/integrations/social/mastodon
M. Instancemastodon-customexternalUrl—/integrations/social/mastodon (same route)
MediummediumcustomFields——
MeWemeweoauthMEWE_APP_ID, MEWE_API_KEY/integrations/social/mewe
MoltbookmoltbookmoltbookClaim——
NostrnostrcustomFields——
PinterestpinterestoauthPINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET/integrations/social/pinterest
RedditredditoauthREDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET/integrations/social/reddit
SlackslackoauthSLACK_ID, SLACK_SECRET/integrations/social/slack *
TelegramtelegramtelegramBotTELEGRAM_TOKEN (optional TELEGRAM_BOT_NAME)— (bot command, no redirect)
ThreadsthreadsoauthTHREADS_APP_ID, THREADS_APP_SECRET/integrations/social/threads *
TiktoktiktokoauthTIKTOK_CLIENT_ID, TIKTOK_CLIENT_SECRET/integrations/social/tiktok *
TikTok (Business)tiktok-businessoauthTIKTOK_BUSINESS_CLIENT_ID, TIKTOK_BUSINESS_CLIENT_SECRET/integrations/social/tiktok-business/ *†
TumblrtumblroauthTUMBLR_CLIENT_ID, TUMBLR_CLIENT_SECRET/integrations/social/tumblr
TwitchtwitchoauthTWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET/integrations/social/twitch
VKvkoauthVK_ID/integrations/social/vk *
WhopwhopoauthWHOP_CLIENT_ID/integrations/social/whop
WordPresswordpresscustomFields——
FarcasterwrapcastsignerNEYNAR_CLIENT_ID, NEYNAR_APP_FID, NEYNAR_APP_MNEMONIC, NEYNAR_SECRET_KEY— (signer approval, no redirect)
XxoauthX_API_KEY, X_API_SECRET/integrations/social/x (or X_URL base)
YouTubeyoutubeoauthYOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET/integrations/social/youtube

Shared credentials and naming traps

TrapDetail
Meta appfacebook, instagram use FACEBOOK_APP_ID / FACEBOOK_APP_SECRET. instagram-standalone uses a separate INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET. Threads uses its own THREADS_APP_ID / THREADS_APP_SECRET.
Google appgmb prefers GOOGLE_GMB_CLIENT_ID / GOOGLE_GMB_CLIENT_SECRET; a missing GMB id falls back to YOUTUBE_CLIENT_ID and a missing GMB secret to YOUTUBE_CLIENT_SECRET, so use one complete pair.
LinkedIn applinkedin and linkedin-page share one app and the same scope set; the organization scopes are required for the personal provider too.
Dribbble spellingThe env vars are DRIBBBLE_CLIENT_ID / DRIBBBLE_CLIENT_SECRET and the callback is /integrations/social/dribbble (two Bs). Some Postiz docs say DRIBBLE_* / .../dribble; those are typos.
Kick secretThe secret variable is KICK_SECRET, not KICK_CLIENT_SECRET.
Discord botDISCORD_CLIENT_ID/DISCORD_CLIENT_SECRET make the provider “configured”, but posting uses DISCORD_BOT_TOKEN_ID; without it channel messages fail.
TikTok vs TikTok BusinessNever reuse one app’s credentials for the other: tiktok-business is a different API host, scope set and redirect URI.
Identifier vs namewrapcast is Farcaster, gmb is Google My Business, mastodon-custom is “M. Instance”, instagram-standalone is the standalone login. These identifiers are also the doc filenames and the platform argument to integrationSchema.

Connecting from an agent

REST

curl -sS -X POST https://social.example.com/public/v1/integrations/x/connect \
  -H "Authorization: Bearer $RUSTIZ_API_KEY" \
  -H 'content-type: application/json' -d '{}'

The response is either {connected:true,...} or {connected:false,pending:true,connectionId,url,message}. Poll GET /public/v1/integrations/connect/<connectionId> (or connectChannelTool with that id) until it reports connected, then read GET /public/v1/integrations for the channel id.

MCP

connectChannelTool arguments:

FieldNotes
providerIdentifier from the table above. Omit when polling connectionId.
connectionIdPoll a pending OAuth/Telegram/Moltbook/Farcaster connection.
externalUrlSelf-hosted instance URL; required for mastodon-custom.
customFieldsCredentials for customFields providers. Telegram also accepts {"chatId":"-100..."}; Moltbook {"apiKey":"..."}; Farcaster {"signerUuid":"..."}.
nameMoltbook agent name (required for a new claim).
descriptionMoltbook agent description (optional).

customFields takes precedence over OAuth for any provider when it is a JSON object, so only send it to providers whose integrationSchema lists customFields.

For customFields providers, the keys and validation regexes are returned by integrationSchema (the same data the provider’s custom_fields() builds); the provider page reproduces them. Credentials are encrypted with JWT_SECRET and never returned.

Channels with inBetweenSteps (Facebook pages, Instagram accounts, LinkedIn companies, YouTube channels, Google Business locations, Tumblr blogs) need a second call:

triggerTool       { integrationId, methodName: "pages", dataSchema: [] }
channelSetupTool  { integrationId, data: { "page": "<id>" } }

methodName must be one of the provider’s declared functions (pages, companies, boards, subreddits, channels, blogs, locations, …). The provider page lists the exact follow-up for each.

Provider page checklist

Every provider page in ./providers/ follows the same shape:

  1. Overview table — identifier, display name, connection mode, capabilities (comments/threads, pending/async publishing, analytics, post analytics, mentions, is_between_steps, convert_to_jpeg, max_concurrent_job, character limit).
  2. Server environment variables — required_env() / required_env_alternatives(), plus optional <PLATFORM>_BASE_URL overrides.
  3. Developer-app setup — portal URL, products/APIs, exact redirect URI (including redirectmeto.com and trailing-slash quirks), exact scopes requested and checked by check_scopes(), and sandbox/test-user vs production review.
  4. Connecting a channel — CLI rustiz channels connect <id> and MCP connectChannelTool, including customFields keys/regexes or triggerTool/channelSetupTool follow-ups.
  5. Post settings and media validation — settings_schema(), validate_settings(), check_validity(), media requirements and plug functions (run_plug / run_post_plug).
  6. Implementation notes — token refresh, rate limits and port-specific quirks.

If a page and the Rust code disagree, the Rust code wins. The provider’s identifier() and ProviderInfo live in crates/rustiz-providers/src/<module>.rs; the connect flows are in crates/rustiz-server/src/connect.rs and oauth.rs.