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

Configuration and environment variables

3. Configuration model

3.1 Precedence and the config file

At startup, before clap parses arguments, rustiz loads a dotenv-style file:

  1. --config <path>
  2. RUSTIZ_CONFIG=<path>
  3. ~/.rustiz/config.env (or %USERPROFILE%\.rustiz\config.env)

A file named explicitly by --config or RUSTIZ_CONFIG that does not exist is a fatal error. A missing default file is fine. rustiz init should be pointed at a directory with --dir, not at a not-yet-existing --config file, because the config file is loaded before the command runs.

The file is loaded before clap parses the command line, so PORT, HOST, DATABASE_URL and RUSTIZ_API_KEY written there are visible to the CLI as normal environment variables.

Variables already present in the real environment are not overwritten by the file. An exported variable wins even when it is empty; an empty value is then treated as unset by the application. To change a setting permanently, edit the file; to override it for one run, export it.

The parser accepts:

  • KEY=value, optional export KEY=value
  • # comments, blank lines
  • single quotes (literal), double quotes (\n, \r, \t, \\, \", \')
  • bare values, with an inline comment starting at whitespace + #
  • duplicate keys: the last one wins
  • a UTF-8 BOM at the start

On Unix, rustiz warns at every start if config.env is readable by group or others. Keep it 0600; it contains JWT_SECRET and the organization API key.

3.2 JWT_SECRET

JWT_SECRET is required by serve, worker and mcp; the process exits with JWT_SECRET must be set otherwise. rustiz init generates 64 random alphanumeric characters (about 380 bits).

It is not a session-signing key in this port. It is the passphrase for the AES-256-CBC (OpenSSL EVP_BytesToKey/MD5, byte-compatible with Postiz) layer that encrypts:

  • customFields credential JSON (stored in Integration.customInstanceDetails)
  • per-instance OAuth client information (customInstanceDetails) for externalUrl/custom-instance providers
  • the pending client_info of an externalUrl OAuth state row

It does not encrypt provider access tokens in this port: the Integration.token / Integration.refreshToken columns are plain text in the SQLite file. That includes OAuth access/refresh tokens and some API keys (for example a Moltbook agent key or a Telegram chat id). Protect the database file itself (file permissions, disk encryption, encrypted backups) and treat a copy of it as a credential dump.

Changing or losing JWT_SECRET makes the encrypted custom-credential channels unreadable (Bluesky, WordPress, Lemmy, Listmonk, Medium, Dev.to, Hashnode, Nostr, Mastodon custom instances, etc.), and those channels must be reconnected. OAuth channels keep working because their tokens are not encrypted with this secret, but reconnecting them is the cleanest response to a suspected secret compromise. There is no supported rotation procedure short of reconnecting every affected channel.

3.3 Database URL

SQLite URLs use the sqlx form:

sqlite:///absolute/path/rustiz.db   # absolute (three slashes)
sqlite://relative/rustiz.db         # relative to the process CWD
sqlite::memory:                     # tests only

rustiz init percent-encodes %, ? and # in the path. The database runs in WAL mode, synchronous=NORMAL, foreign keys on, busy timeout 10s, and up to 8 pooled connections. Migrations run automatically on every connection open; rustiz migrate is a convenience that opens the database, migrates and exits.

Because WAL mode writes rustiz.db-wal and rustiz.db-shm, a naive copy of only rustiz.db while the server runs can lose recent transactions. Use sqlite3 ... ".backup ..." or stop the process (§8.3).


4. Complete environment variable reference

This section covers variables read by the core, server, engine and CLI outside individual social providers. Provider credentials (X_API_KEY, FACEBOOK_APP_ID, …) are listed in the master provider table and on each provider page.

4.1 Core

VariableRequiredDefaultPurpose
FRONTEND_URLyes in productionhttp://localhost:5000Public origin as the browser/agent sees it. Base of every OAuth redirect URI, /uploads/... URLs, email links, MCP metadata and WWW-Authenticate. A trailing slash is removed. Changing it invalidates every registered redirect URI.
DATABASE_URLnosqlite://rustiz.dbSQLite URL. Docker image sets sqlite:///data/rustiz.db.
JWT_SECRETyes for serve/worker/mcpgenerated by initEncryption key for customFields credentials and custom-instance OAuth client data; provider access tokens are not encrypted with it. See §3.2.
PORTno5000HTTP listen port (serve only, via clap/env).
HOSTno0.0.0.0HTTP bind address. Use 127.0.0.1 behind a local reverse proxy.
ALLOW_PRIVATE_NETWORKnofalsetrue/1 disables the SSRF guard globally: outbound HTTP may resolve to loopback, private, link-local and metadata addresses. Only for a trusted single-tenant network.
UPLOAD_DIRECTORYfor local storageunsetDirectory for local media, and the root served at /uploads. Without it, local uploads fail and generated JPEGs/thumbnails cannot be stored.
MAX_UPLOAD_MBno1024Maximum request body in MiB for every route (not just multipart). Also cap it in the reverse proxy.
RUST_LOGnoinfo,sqlx=warn,tower_http=infotracing env filter. Logs go to stderr; stdout is reserved for the MCP stdio protocol.

4.2 CLI, MCP and config discovery

VariableRequiredDefaultPurpose
RUSTIZ_CONFIGno~/.rustiz/config.envConfig file to load before argument parsing.
RUSTIZ_URLnounsetCLI only: talk to this server instead of the local database. Must be an http(s):// origin, no /public/v1 suffix; the CLI appends it. Requires RUSTIZ_API_KEY. 300-second client timeout.
RUSTIZ_API_KEYwith RUSTIZ_URLfrom config fileOrganization API key for the CLI and for rustiz mcp.
HOME / USERPROFILEnoOS valueUsed to find ~/.rustiz.

With RUSTIZ_URL, the CLI only has the remote REST API. A channels connect OAuth flow cannot watch the server’s callback or detect the new channel; it prints the login URL and returns. Use local database mode with a loopback HTTP FRONTEND_URL if you want the CLI to wait for the login.

4.3 Storage

STORAGE_PROVIDER selects the backend: local (default), cloudflare (Cloudflare R2) or s3 (any S3-compatible store, including MinIO). An invalid value is a fatal error for the upload route and object-storage features.

VariableRequiredDefaultPurpose
STORAGE_PROVIDERnolocallocal, cloudflare or s3.

Local

VariableRequiredPurpose
UPLOAD_DIRECTORYyesWrites <yyyy>/<mm>/<dd>/<id>.<ext>; served at ${FRONTEND_URL}/uploads/.... Must be on persistent storage.

Cloudflare R2

VariableRequiredDefaultPurpose
CLOUDFLARE_ACCOUNT_IDyes—R2 account id; endpoint is built as https://<account>.r2.cloudflarestorage.com.
CLOUDFLARE_ACCESS_KEYyes—R2 access key id.
CLOUDFLARE_SECRET_ACCESS_KEYyes—R2 secret access key.
CLOUDFLARE_BUCKETNAMEyes—Bucket name.
CLOUDFLARE_REGIONnoautoR2 region.
CLOUDFLARE_BUCKET_URLyes—Public HTTPS base URL of the bucket/custom domain, without a trailing slash. Stored object URLs are <bucket-url>/<key>.

S3 and S3-compatible stores

Aliases are checked in the order shown; AWS_* wins over S3_* when both are set.

VariableRequiredDefaultPurpose
AWS_BUCKETNAME / S3_BUCKETNAMEyes—Bucket name.
AWS_REGION / S3_REGIONnous-east-1Region.
AWS_ENDPOINT / S3_ENDPOINTnoAWS defaultCustom endpoint (MinIO, Backblaze, etc.). An http:// endpoint enables plain HTTP for the object store.
AWS_ACCESS_KEY_ID / S3_ACCESS_KEY_IDnoenv/default chainAccess key.
AWS_SECRET_ACCESS_KEY / S3_SECRET_ACCESS_KEYnoenv/default chainSecret key.
AWS_SESSION_TOKEN / S3_SESSION_TOKENno—Session token for temporary credentials.
AWS_BUCKET_URL / S3_BUCKET_URLnoderivedPublic HTTPS base URL. If unset: AWS_ENDPOINT/<bucket> when an endpoint is set, otherwise https://<bucket>.s3.<region>.amazonaws.com.

Path-style requests are used (with_virtual_hosted_style_request(false)), which is what MinIO expects. The bucket (or a CDN in front of it) must serve objects publicly; the app does not generate signed URLs.

4.4 Email

Email is optional: with EMAIL_PROVIDER unset or unknown, nothing is sent and the process logs no email provider configured. Notification rows are still written to the database. To actually deliver, you also need both EMAIL_FROM_ADDRESS and EMAIL_FROM_NAME; a missing sender identity or an address without @ is skipped silently (a log line is emitted).

VariableRequiredDefaultPurpose
EMAIL_PROVIDERyes to sendunset = disabledresend, nodemailer/smtp, or ses. Any other value disables email.
EMAIL_FROM_ADDRESSyes—From: address. Verify the domain with Resend/SES/SMTP provider.
EMAIL_FROM_NAMEyes—From: display name.
EMAIL_RATE_INTERVAL_MSnopriority 1050 ms, bulk 1750 msMinimum spacing between queued sends. The default is 3×/5× the 350 ms base interval.

Resend

VariableRequiredDefaultPurpose
RESEND_API_KEYyes—Resend API key; sent as Authorization: Bearer.
RESEND_API_BASE_URLnohttps://api.resend.comBase URL override (tests/mocks). The path /emails is appended.

SMTP / nodemailer

VariableRequiredDefaultPurpose
EMAIL_HOSTyes—SMTP host.
EMAIL_PORTno587SMTP port.
EMAIL_SECUREnofalsetrue = implicit TLS (SMTPS); false = STARTTLS.
EMAIL_USERno—SMTP username. Without user+pass the transport is unauthenticated.
EMAIL_PASSno—SMTP password.

Amazon SES

EMAIL_PROVIDER=ses uses SES’s SMTP interface, not the SES API. If either SES_SMTP_HOST or SES_SMTP_USER is set, the SMTP aliases are used; otherwise the host is derived from the region and credentials fall back to the SES access key pair.

VariableRequiredDefaultPurpose
SES_REGIONyes unless SES_SMTP_HOST or SES_SMTP_USER is setus-east-1Builds email-smtp.<region>.amazonaws.com.
SES_SMTP_HOSTnoemail-smtp.<region>.amazonaws.comOverride SMTP host.
SES_SMTP_PORTno465SMTP port.
SES_SMTP_SECUREnotrueImplicit TLS; false = STARTTLS.
SES_SMTP_USER / SES_SMTP_USERNAMEconditional—SMTP username; falls back to SES_ACCESS_KEY.
SES_SMTP_PASS / SES_SMTP_PASSWORDconditional—SMTP password; falls back to SES_SECRET_ACCESS_KEY.
SES_ACCESS_KEYconditional—SES SMTP username alias.
SES_SECRET_ACCESS_KEYconditional—SES SMTP password alias.

Short-link replacement is opt-in per post (shortLink: true). The first configured provider wins in this exact order: Dub → Short.io → Kutt → LinkDrip. With none configured, links are left untouched.

VariableRequiredDefaultPurpose
DUB_TOKENfor Dub—Dub API token (Authorization: Bearer). Presence selects Dub.
DUB_API_ENDPOINTnohttps://api.dub.coDub API base.
DUB_API_ENDPOINT_BASE_URLno—Fallback for DUB_API_ENDPOINT (the code appends _BASE_URL).
DUB_SHORT_LINK_DOMAINnodub.shDomain used for links and for “already shortened” detection.
SHORT_IO_SECRET_KEYfor Short.io—Short.io secret key.
SHORT_IO_API_ENDPOINTnohttps://api.short.ioShort.io API base.
SHORT_IO_API_ENDPOINT_BASE_URLno—Fallback for SHORT_IO_API_ENDPOINT.
KUTT_API_KEYfor Kutt—Sent as X-API-Key.
KUTT_API_ENDPOINTnohttps://kutt.it/api/v2Kutt API base.
KUTT_API_ENDPOINT_BASE_URLno—Fallback for KUTT_API_ENDPOINT.
KUTT_SHORT_LINK_DOMAINnokutt.itKutt domain.
LINK_DRIP_API_KEYfor LinkDrip—Sent as Authorization: Bearer.
LINK_DRIP_API_ENDPOINTnohttps://api.linkdrip.com/v1LinkDrip API base.
LINK_DRIP_API_ENDPOINT_BASE_URLno—Fallback for LINK_DRIP_API_ENDPOINT.
LINK_DRIP_SHORT_LINK_DOMAINnodripl.inkLinkDrip domain.

Notes:

  • Short.io’s domain is hard-coded as short.io; it has no domain env.
  • Short.io statistics use a hard-coded https://statistics.short.io/... host; the link listing reads up to 10 pages of 150 links.
  • LinkDrip expansion returns an empty string and its click count is always "0"; only creation is implemented.
  • Dub sends the organization id as tenantId.

4.6 HTTP limits and proxies

VariableRequiredDefaultPurpose
API_LIMITno90Per-organization, per-hour limit for POST routes under /public/v1 whose path contains /posts (create post). 0 disables; invalid values fall back to 90 with a warning.
REQUEST_LIMITno10000Per-organization, per-hour limit for /mcp and the other /public/v1 routes. 0 disables; invalid values fall back.
TRUSTED_PROXY_HOPSno0Number of trusted proxies that append to X-Forwarded-For. 0 ignores XFF and uses the socket peer. With one Nginx/Caddy hop set 1; with Cloudflare + Nginx set 2. The code trusts the Nth address from the right without verifying that the peer is a proxy.

The limiter is a fixed one-hour window held in process memory. Counts are not shared between replicas and reset on restart. X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are sent on allowed responses; a blocked response is 429 with Retry-After. MCP gets a JSON-RPC error body; REST gets {"statusCode":429,"message":"ThrottlerException: Too Many Requests"}.

OAuth credential endpoints have fixed per-IP limits that cannot be changed:

EndpointLimit
POST /oauth/authorize30/hour/address
POST /oauth/register30/hour/address
POST /oauth/token120/hour/address

4.7 MCP

VariableRequiredDefaultPurpose
MCP_ALLOWED_HOSTSnoFRONTEND_URL host, localhost, 127.0.0.1Comma-separated extra Host header values accepted by the MCP transport (DNS-rebinding protection). An entry without a port matches any port; host:port restricts it. Add [::1] for IPv6 loopback and the exact internal host if a proxy rewrites Host.
MCP_SUBSCRIPTIONS_POLL_MSno2000Poll interval for subscriptions/listen post-state hints. Clamped to 20–60000 ms.
MCP_SUBSCRIPTIONS_MAX_STREAMSno4Concurrent subscriptions/listen streams per organization. Clamped to 1–64.
MCP_SUBSCRIPTIONS_SEND_TIMEOUT_MSno10000How long a notification send may block before the slow stream is dropped. Clamped to 100–120000 ms.

subscriptions/listen reads the database, not an in-process bus, so it also sees changes made by a separate rustiz worker. Quick changes between polls collapse into the last one; deletions are not reported.

4.8 Worker, background jobs and media processing

VariableRequiredDefaultPurpose
WORKER_CONCURRENCYno32Global concurrent jobs per worker process. Per-provider semaphores apply on top (max_concurrent_job from the provider), and the internal main queue is capped at 4.
FFMPEG_BINARYnoffmpegffmpeg executable used for video thumbnails. It is probed with ffmpeg -version; absence is not an error.
AUTOPOST_INTERVAL_MSno3600000RSS autopost polling interval.
DIGEST_DELAY_MSno3600000Notification digest delay.
STREAK_REMINDER_MSno79200000 (22 h)When the streak reminder is queued.
STREAK_GRACE_MSno7200000 (2 h)Extra grace before a streak is considered broken.

4.9 Upload validation and compatibility

VariableRequiredDefaultPurpose
RESTRICT_UPLOAD_DOMAINSnounsetA single substring that media paths must contain in some providers’ post validation (X, Dev.to, Hashnode). It is not an allowlist for /public/v1/upload-from-url; that endpoint uses the SSRF guard instead. Despite the plural name, the implementation is a substring check.
DISABLE_SSRF_PROTECTIONnofalseBluesky-only compatibility flag: when true, the Bluesky service URL check accepts a non-public address. It does not disable the global HTTP guard; use ALLOW_PRIVATE_NETWORK for that (and only on a trusted network).
NEXT_PUBLIC_BACKEND_URLnounsetUsed by the Hashnode provider when it needs to build an absolute media URL. In this port it should be the same public origin as FRONTEND_URL.
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORYnounsetUpload path prefix used with NEXT_PUBLIC_BACKEND_URL by the Hashnode provider (normally /uploads).

4.10 Provider credentials

Every OAuth/API credential is documented per provider. The shared groups that catch people out:

  • Facebook Page + Instagram (Facebook Business) use FACEBOOK_APP_ID / FACEBOOK_APP_SECRET, one Meta app.
  • LinkedIn + LinkedIn Page use LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET, one app and one scope set.
  • Google My Business accepts 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.

See Documentation overview.