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_SECRETencryptscustomFieldscredentials and custom-instance OAuth client data. Provider access tokens in theIntegration.token/refreshTokencolumns 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
| Document | What it covers |
|---|---|
| Quick start | Build, init, connect an agent and post, in five minutes. |
| MCP setup | MCP setup for Claude Desktop, Claude Code, Cursor and remote clients; the 31 tools; the posting flow. |
| CLI | Every CLI command, local vs remote mode, JSON output and exit codes. |
| REST API | /public/v1 endpoints, auth, rate limits and errors. |
| Benchmarks | The measured numbers used on the website, and how to reproduce them. |
| Self-hosting | Install (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 README | Project overview, measured performance, CLI/MCP quick reference, rate limits. |
| Porting status | What 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.
| Mode | Meaning | Examples |
|---|---|---|
oauth | The server returns a login URL; a human authorizes; the platform redirects to FRONTEND_URL/integrations/social/<identifier>. | X, LinkedIn, Reddit, YouTube, Mastodon |
customFields | The 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 |
externalUrl | The 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 |
telegramBot | A human sends /connect <word> to the deployment’s bot, or the agent supplies the numeric chatId. | Telegram |
moltbookClaim | The server registers an agent; a human opens the claim URL; the agent polls. | Moltbook |
signer | The 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).
| Provider | Identifier | What the user supplies |
|---|---|---|
| Bluesky | bluesky | Service URL (default https://bsky.social), handle, app password |
| Dev.to | devto | API key |
| Hashnode | hashnode | API key |
| Lemmy | lemmy | Instance URL, username, password |
| ListMonk | listmonk | URL, username, password |
| M. Instance | mastodon-custom | Instance URL; the server registers an OAuth app on it |
| Medium | medium | API key |
| Moltbook | moltbook | Nothing; the server registers an agent and a human claims it |
| Nostr | nostr | 64-character hex private key (nsec is not accepted) |
| WordPress | wordpress | Site 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.
| Provider | Identifier | What you create |
|---|---|---|
| Discord | discord | Application + bot; client id/secret and bot token |
| Kick | kick | Developer app (OAuth 2.1 + PKCE) |
| Mastodon | mastodon | OAuth app on the instance configured in MASTODON_URL (POST /api/v1/apps); use mastodon-custom for arbitrary instances |
| Slack | slack | Slack app with bot scopes and an icon |
| Telegram | telegram | Bot from BotFather |
| Tumblr | tumblr | OAuth app |
| Twitch | twitch | Developer console app |
| VK | vk | VK ID app |
| Whop | whop | OAuth 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.
| Provider | Identifier | Gate |
|---|---|---|
| Dribbble | dribbble | Application registration/review; scoped tokens |
| Facebook Page | facebook | Meta app in Development mode works for roles/testers; Live mode needs business verification and advanced permissions |
| Instagram (Facebook Business) | instagram | Same Meta app; professional account linked to a Facebook Page; advanced permissions |
| Instagram (Standalone) | instagram-standalone | Separate Instagram app; professional account; testers until review |
linkedin | Products: Share on LinkedIn, Sign In with LinkedIn (OIDC), Advertising API; the org scopes need the Advertising API request | |
| LinkedIn Page | linkedin-page | Same app and scopes; must administer a company page |
pinterest | Trial access works for the app owner; standard access is reviewed | |
reddit | Web app is self-serve, but API access is gated by Reddit’s Responsible Builder review; commercial use is a separate contract | |
| Threads | threads | Meta app + Threads API use case; tester invites until the app is live |
| TikTok | tiktok | Developer app + Content Posting API; unaudited Direct Post is private and limited to a few users/day; audit unlocks privacy levels |
| X | x | OAuth 1.0a app works on the free tier with strict monthly write limits; production volume needs a paid API tier |
| YouTube | youtube | Google 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
| Provider | Identifier | Gate |
|---|---|---|
| Google My Business | gmb | Google My Business API access request (days to a week) and a verified Business Profile |
| MeWe | mewe | MeWe Developer Program is in beta; applications are reviewed and spots are limited |
| TikTok Business | tiktok-business | TikTok Business/Ads developer app and business account; separate credentials and a trailing-slash redirect URI |
| Farcaster | wrapcast | Neynar 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 name | Identifier | Connection | Required server env vars | Redirect URI path |
|---|---|---|---|---|
| Bluesky | bluesky | customFields | — | — (no browser redirect) |
| Dev.to | devto | customFields | — | — |
| Discord | discord | oauth | DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET (set DISCORD_BOT_TOKEN_ID too; posting uses the bot token) | /integrations/social/discord |
| Dribbble | dribbble | oauth | DRIBBBLE_CLIENT_ID, DRIBBBLE_CLIENT_SECRET | /integrations/social/dribbble |
| Facebook Page | facebook | oauth | FACEBOOK_APP_ID, FACEBOOK_APP_SECRET ‡ | /integrations/social/facebook |
| Google My Business | gmb | oauth | GOOGLE_GMB_CLIENT_ID + GOOGLE_GMB_CLIENT_SECRET, or YOUTUBE_CLIENT_ID + YOUTUBE_CLIENT_SECRET ‡ | /integrations/social/gmb |
| Hashnode | hashnode | customFields | — | — |
| Instagram (Facebook Business) | instagram | oauth | FACEBOOK_APP_ID, FACEBOOK_APP_SECRET ‡ | /integrations/social/instagram |
| Instagram (Standalone) | instagram-standalone | oauth | INSTAGRAM_APP_ID, INSTAGRAM_APP_SECRET | /integrations/social/instagram-standalone * |
| Kick | kick | oauth | KICK_CLIENT_ID, KICK_SECRET | /integrations/social/kick |
| Lemmy | lemmy | customFields | — | — |
linkedin | oauth | LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET ‡ | /integrations/social/linkedin | |
| LinkedIn Page | linkedin-page | oauth | LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET ‡ | /integrations/social/linkedin-page |
| ListMonk | listmonk | customFields | — | — |
| Mastodon | mastodon | oauth | MASTODON_CLIENT_ID, MASTODON_CLIENT_SECRET (optional MASTODON_URL, default https://mastodon.social) | /integrations/social/mastodon |
| M. Instance | mastodon-custom | externalUrl | — | /integrations/social/mastodon (same route) |
| Medium | medium | customFields | — | — |
| MeWe | mewe | oauth | MEWE_APP_ID, MEWE_API_KEY | /integrations/social/mewe |
| Moltbook | moltbook | moltbookClaim | — | — |
| Nostr | nostr | customFields | — | — |
pinterest | oauth | PINTEREST_CLIENT_ID, PINTEREST_CLIENT_SECRET | /integrations/social/pinterest | |
reddit | oauth | REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET | /integrations/social/reddit | |
| Slack | slack | oauth | SLACK_ID, SLACK_SECRET | /integrations/social/slack * |
| Telegram | telegram | telegramBot | TELEGRAM_TOKEN (optional TELEGRAM_BOT_NAME) | — (bot command, no redirect) |
| Threads | threads | oauth | THREADS_APP_ID, THREADS_APP_SECRET | /integrations/social/threads * |
| Tiktok | tiktok | oauth | TIKTOK_CLIENT_ID, TIKTOK_CLIENT_SECRET | /integrations/social/tiktok * |
| TikTok (Business) | tiktok-business | oauth | TIKTOK_BUSINESS_CLIENT_ID, TIKTOK_BUSINESS_CLIENT_SECRET | /integrations/social/tiktok-business/ *† |
| Tumblr | tumblr | oauth | TUMBLR_CLIENT_ID, TUMBLR_CLIENT_SECRET | /integrations/social/tumblr |
| Twitch | twitch | oauth | TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET | /integrations/social/twitch |
| VK | vk | oauth | VK_ID | /integrations/social/vk * |
| Whop | whop | oauth | WHOP_CLIENT_ID | /integrations/social/whop |
| WordPress | wordpress | customFields | — | — |
| Farcaster | wrapcast | signer | NEYNAR_CLIENT_ID, NEYNAR_APP_FID, NEYNAR_APP_MNEMONIC, NEYNAR_SECRET_KEY | — (signer approval, no redirect) |
| X | x | oauth | X_API_KEY, X_API_SECRET | /integrations/social/x (or X_URL base) |
| YouTube | youtube | oauth | YOUTUBE_CLIENT_ID, YOUTUBE_CLIENT_SECRET | /integrations/social/youtube |
Shared credentials and naming traps
| Trap | Detail |
|---|---|
| Meta app | facebook, 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 app | gmb 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 app | linkedin and linkedin-page share one app and the same scope set; the organization scopes are required for the personal provider too. |
| Dribbble spelling | The 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 secret | The secret variable is KICK_SECRET, not KICK_CLIENT_SECRET. |
| Discord bot | DISCORD_CLIENT_ID/DISCORD_CLIENT_SECRET make the provider “configured”, but posting uses DISCORD_BOT_TOKEN_ID; without it channel messages fail. |
| TikTok vs TikTok Business | Never reuse one app’s credentials for the other: tiktok-business is a different API host, scope set and redirect URI. |
| Identifier vs name | wrapcast 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:
| Field | Notes |
|---|---|
provider | Identifier from the table above. Omit when polling connectionId. |
connectionId | Poll a pending OAuth/Telegram/Moltbook/Farcaster connection. |
externalUrl | Self-hosted instance URL; required for mastodon-custom. |
customFields | Credentials for customFields providers. Telegram also accepts {"chatId":"-100..."}; Moltbook {"apiKey":"..."}; Farcaster {"signerUuid":"..."}. |
name | Moltbook agent name (required for a new claim). |
description | Moltbook 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:
- 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). - Server environment variables —
required_env()/required_env_alternatives(), plus optional<PLATFORM>_BASE_URLoverrides. - Developer-app setup — portal URL, products/APIs, exact redirect URI
(including
redirectmeto.comand trailing-slash quirks), exact scopes requested and checked bycheck_scopes(), and sandbox/test-user vs production review. - Connecting a channel — CLI
rustiz channels connect <id>and MCPconnectChannelTool, includingcustomFieldskeys/regexes ortriggerTool/channelSetupToolfollow-ups. - Post settings and media validation —
settings_schema(),validate_settings(),check_validity(), media requirements and plug functions (run_plug/run_post_plug). - 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.