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:
--config <path>RUSTIZ_CONFIG=<path>~/.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, optionalexport 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:
customFieldscredential JSON (stored inIntegration.customInstanceDetails)- per-instance OAuth client information (
customInstanceDetails) forexternalUrl/custom-instance providers - the pending
client_infoof anexternalUrlOAuth 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
| Variable | Required | Default | Purpose |
|---|---|---|---|
FRONTEND_URL | yes in production | http://localhost:5000 | Public 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_URL | no | sqlite://rustiz.db | SQLite URL. Docker image sets sqlite:///data/rustiz.db. |
JWT_SECRET | yes for serve/worker/mcp | generated by init | Encryption key for customFields credentials and custom-instance OAuth client data; provider access tokens are not encrypted with it. See §3.2. |
PORT | no | 5000 | HTTP listen port (serve only, via clap/env). |
HOST | no | 0.0.0.0 | HTTP bind address. Use 127.0.0.1 behind a local reverse proxy. |
ALLOW_PRIVATE_NETWORK | no | false | true/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_DIRECTORY | for local storage | unset | Directory for local media, and the root served at /uploads. Without it, local uploads fail and generated JPEGs/thumbnails cannot be stored. |
MAX_UPLOAD_MB | no | 1024 | Maximum request body in MiB for every route (not just multipart). Also cap it in the reverse proxy. |
RUST_LOG | no | info,sqlx=warn,tower_http=info | tracing env filter. Logs go to stderr; stdout is reserved for the MCP stdio protocol. |
4.2 CLI, MCP and config discovery
| Variable | Required | Default | Purpose |
|---|---|---|---|
RUSTIZ_CONFIG | no | ~/.rustiz/config.env | Config file to load before argument parsing. |
RUSTIZ_URL | no | unset | CLI 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_KEY | with RUSTIZ_URL | from config file | Organization API key for the CLI and for rustiz mcp. |
HOME / USERPROFILE | no | OS value | Used 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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
STORAGE_PROVIDER | no | local | local, cloudflare or s3. |
Local
| Variable | Required | Purpose |
|---|---|---|
UPLOAD_DIRECTORY | yes | Writes <yyyy>/<mm>/<dd>/<id>.<ext>; served at ${FRONTEND_URL}/uploads/.... Must be on persistent storage. |
Cloudflare R2
| Variable | Required | Default | Purpose |
|---|---|---|---|
CLOUDFLARE_ACCOUNT_ID | yes | — | R2 account id; endpoint is built as https://<account>.r2.cloudflarestorage.com. |
CLOUDFLARE_ACCESS_KEY | yes | — | R2 access key id. |
CLOUDFLARE_SECRET_ACCESS_KEY | yes | — | R2 secret access key. |
CLOUDFLARE_BUCKETNAME | yes | — | Bucket name. |
CLOUDFLARE_REGION | no | auto | R2 region. |
CLOUDFLARE_BUCKET_URL | yes | — | 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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
AWS_BUCKETNAME / S3_BUCKETNAME | yes | — | Bucket name. |
AWS_REGION / S3_REGION | no | us-east-1 | Region. |
AWS_ENDPOINT / S3_ENDPOINT | no | AWS default | Custom endpoint (MinIO, Backblaze, etc.). An http:// endpoint enables plain HTTP for the object store. |
AWS_ACCESS_KEY_ID / S3_ACCESS_KEY_ID | no | env/default chain | Access key. |
AWS_SECRET_ACCESS_KEY / S3_SECRET_ACCESS_KEY | no | env/default chain | Secret key. |
AWS_SESSION_TOKEN / S3_SESSION_TOKEN | no | — | Session token for temporary credentials. |
AWS_BUCKET_URL / S3_BUCKET_URL | no | derived | Public 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).
| Variable | Required | Default | Purpose |
|---|---|---|---|
EMAIL_PROVIDER | yes to send | unset = disabled | resend, nodemailer/smtp, or ses. Any other value disables email. |
EMAIL_FROM_ADDRESS | yes | — | From: address. Verify the domain with Resend/SES/SMTP provider. |
EMAIL_FROM_NAME | yes | — | From: display name. |
EMAIL_RATE_INTERVAL_MS | no | priority 1050 ms, bulk 1750 ms | Minimum spacing between queued sends. The default is 3×/5× the 350 ms base interval. |
Resend
| Variable | Required | Default | Purpose |
|---|---|---|---|
RESEND_API_KEY | yes | — | Resend API key; sent as Authorization: Bearer. |
RESEND_API_BASE_URL | no | https://api.resend.com | Base URL override (tests/mocks). The path /emails is appended. |
SMTP / nodemailer
| Variable | Required | Default | Purpose |
|---|---|---|---|
EMAIL_HOST | yes | — | SMTP host. |
EMAIL_PORT | no | 587 | SMTP port. |
EMAIL_SECURE | no | false | true = implicit TLS (SMTPS); false = STARTTLS. |
EMAIL_USER | no | — | SMTP username. Without user+pass the transport is unauthenticated. |
EMAIL_PASS | no | — | 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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
SES_REGION | yes unless SES_SMTP_HOST or SES_SMTP_USER is set | us-east-1 | Builds email-smtp.<region>.amazonaws.com. |
SES_SMTP_HOST | no | email-smtp.<region>.amazonaws.com | Override SMTP host. |
SES_SMTP_PORT | no | 465 | SMTP port. |
SES_SMTP_SECURE | no | true | Implicit TLS; false = STARTTLS. |
SES_SMTP_USER / SES_SMTP_USERNAME | conditional | — | SMTP username; falls back to SES_ACCESS_KEY. |
SES_SMTP_PASS / SES_SMTP_PASSWORD | conditional | — | SMTP password; falls back to SES_SECRET_ACCESS_KEY. |
SES_ACCESS_KEY | conditional | — | SES SMTP username alias. |
SES_SECRET_ACCESS_KEY | conditional | — | SES SMTP password alias. |
4.5 Short links
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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
DUB_TOKEN | for Dub | — | Dub API token (Authorization: Bearer). Presence selects Dub. |
DUB_API_ENDPOINT | no | https://api.dub.co | Dub API base. |
DUB_API_ENDPOINT_BASE_URL | no | — | Fallback for DUB_API_ENDPOINT (the code appends _BASE_URL). |
DUB_SHORT_LINK_DOMAIN | no | dub.sh | Domain used for links and for “already shortened” detection. |
SHORT_IO_SECRET_KEY | for Short.io | — | Short.io secret key. |
SHORT_IO_API_ENDPOINT | no | https://api.short.io | Short.io API base. |
SHORT_IO_API_ENDPOINT_BASE_URL | no | — | Fallback for SHORT_IO_API_ENDPOINT. |
KUTT_API_KEY | for Kutt | — | Sent as X-API-Key. |
KUTT_API_ENDPOINT | no | https://kutt.it/api/v2 | Kutt API base. |
KUTT_API_ENDPOINT_BASE_URL | no | — | Fallback for KUTT_API_ENDPOINT. |
KUTT_SHORT_LINK_DOMAIN | no | kutt.it | Kutt domain. |
LINK_DRIP_API_KEY | for LinkDrip | — | Sent as Authorization: Bearer. |
LINK_DRIP_API_ENDPOINT | no | https://api.linkdrip.com/v1 | LinkDrip API base. |
LINK_DRIP_API_ENDPOINT_BASE_URL | no | — | Fallback for LINK_DRIP_API_ENDPOINT. |
LINK_DRIP_SHORT_LINK_DOMAIN | no | dripl.ink | LinkDrip 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
| Variable | Required | Default | Purpose |
|---|---|---|---|
API_LIMIT | no | 90 | Per-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_LIMIT | no | 10000 | Per-organization, per-hour limit for /mcp and the other /public/v1 routes. 0 disables; invalid values fall back. |
TRUSTED_PROXY_HOPS | no | 0 | Number 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:
| Endpoint | Limit |
|---|---|
POST /oauth/authorize | 30/hour/address |
POST /oauth/register | 30/hour/address |
POST /oauth/token | 120/hour/address |
4.7 MCP
| Variable | Required | Default | Purpose |
|---|---|---|---|
MCP_ALLOWED_HOSTS | no | FRONTEND_URL host, localhost, 127.0.0.1 | Comma-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_MS | no | 2000 | Poll interval for subscriptions/listen post-state hints. Clamped to 20–60000 ms. |
MCP_SUBSCRIPTIONS_MAX_STREAMS | no | 4 | Concurrent subscriptions/listen streams per organization. Clamped to 1–64. |
MCP_SUBSCRIPTIONS_SEND_TIMEOUT_MS | no | 10000 | How 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
| Variable | Required | Default | Purpose |
|---|---|---|---|
WORKER_CONCURRENCY | no | 32 | Global 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_BINARY | no | ffmpeg | ffmpeg executable used for video thumbnails. It is probed with ffmpeg -version; absence is not an error. |
AUTOPOST_INTERVAL_MS | no | 3600000 | RSS autopost polling interval. |
DIGEST_DELAY_MS | no | 3600000 | Notification digest delay. |
STREAK_REMINDER_MS | no | 79200000 (22 h) | When the streak reminder is queued. |
STREAK_GRACE_MS | no | 7200000 (2 h) | Extra grace before a streak is considered broken. |
4.9 Upload validation and compatibility
| Variable | Required | Default | Purpose |
|---|---|---|---|
RESTRICT_UPLOAD_DOMAINS | no | unset | A 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_PROTECTION | no | false | Bluesky-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_URL | no | unset | Used 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_DIRECTORY | no | unset | Upload 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 toYOUTUBE_CLIENT_ID, and a missing GMB secret toYOUTUBE_CLIENT_SECRET.