Appearance
Configuration
Every environment variable Postmill recognises, sourced from .env.example. All variables are read at boot time. Most feature-specific provider credentials (channel OAuth apps, AI providers, storage, short links) are configured per-organization in-app; this page covers the deployment-level variables.
Required
| Variable | Default | Purpose |
|---|---|---|
DATABASE_URL | — | PostgreSQL connection string for the application database |
REDIS_URL | — | Redis connection string. Use redis:// for local Redis or rediss:// for Upstash / TLS endpoints |
JWT_SECRET | — | Secret key for signing JWT tokens; also used as the encryption key fallback |
FRONTEND_URL | — | Public-facing URL of the application (e.g. https://postmill.example.com) |
NEXT_PUBLIC_BACKEND_URL | — | Public URL of the backend API (e.g. https://postmill.example.com/api) |
BACKEND_INTERNAL_URL | — | Internal URL for backend-to-backend calls (e.g. http://localhost:3000) |
MAIN_URL | — | Alternative public URL, used in Docker Compose alongside FRONTEND_URL |
IS_GENERAL | — | Must be true for standard self-hosted deployments |
Storage
| Variable | Default | Purpose |
|---|---|---|
UPLOAD_DIRECTORY | — | Local path for file uploads (e.g. /uploads). Avatars and app-internal images always use LOCAL |
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY | — | Public URL path serving uploads (e.g. /uploads) |
MEDIA_UPLOAD_MAX_BYTES | 1073741824 | Maximum upload file size for /files/upload-server (default 1 GB) |
LOCAL_STORAGE_QUOTA_GB | 5 | Default soft quota for each org's local storage, in GB |
Email
Pluggable provider system with 6 adapters: Resend, SendGrid, Mailgun, Postmark, Amazon SES, and SMTP. The active provider is selected globally via EMAIL_PROVIDER; unset/unknown → email is off (users activate automatically).
| Variable | Default | Purpose |
|---|---|---|
EMAIL_PROVIDER | — | resend, sendgrid, mailgun, postmark, ses, or smtp |
EMAIL_API_KEY | — | API key for Resend, SendGrid, Mailgun, or Postmark |
EMAIL_FROM_ADDRESS | — | Sender email address |
EMAIL_FROM_NAME | — | Sender display name |
EMAIL_WEBHOOK_SECRET | — | Signing secret / verification key for the active provider's webhook |
EMAIL_MAILGUN_DOMAIN | — | Mailgun sending domain (required for mailgun) |
EMAIL_REGION | us | API region for Mailgun (us/eu) and SES region |
EMAIL_SES_ACCESS_KEY_ID | — | SES IAM access key (falls back to AWS_* env vars) |
EMAIL_SES_SECRET_ACCESS_KEY | — | SES IAM secret key (falls back to AWS_* env vars) |
EMAIL_SMTP_HOST | — | SMTP server hostname (required for smtp) |
ADMIN_NOTIFICATIONS_EMAIL | — | Operator address that receives one alert per new tenant signup (the first/primary user who creates an org). Unset = no admin alerts |
EMAIL_SMTP_PORT | 587 | SMTP server port |
EMAIL_SMTP_SECURE | false | Use TLS for SMTP |
EMAIL_SMTP_USER | — | SMTP authentication username (optional for open relays) |
EMAIL_SMTP_PASS | — | SMTP authentication password |
EMAIL_LOG_RETENTION_DAYS | 90 | Days to keep email log metadata before pruning |
DISABLE_REGISTRATION | false | Set to true to disable self-registration. The first user of an empty instance can still register, and GENERIC OIDC sign-ins still provision |
DISALLOW_PLUS | false | Set to true to reject email/password (LOCAL) registrations whose address contains a + |
The webhook endpoint is at POST /webhooks/email, signature-verified, and registered outside CSRF (same as Stripe). SES uses SNS topic verification; EMAIL_WEBHOOK_SECRET can optionally hold the expected SNS TopicArn.
Provider credential setup
| Provider | API key source | Webhook signing secret |
|---|---|---|
| Resend | Create an API key at resend.com | In webhook settings on the Resend dashboard |
| SendGrid | Create a "Full Access" API key at sendgrid.com/settings/api_keys | Enable Event Webhook and copy the Verification Key |
| Mailgun | Use SMTP credentials or create a Mailgun API key | Sent via the Mailgun webhook setup page. Also set EMAIL_MAILGUN_DOMAIN and EMAIL_REGION |
| Postmark | Create a server API token at postmarkapp.com | Generated when creating a webhook in the server settings |
| Amazon SES | IAM credentials, or fall back to AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN | SES uses SNS. Subscribe POST /webhooks/email to the SNS topic; EMAIL_WEBHOOK_SECRET can pin the TopicArn |
| SMTP | No API key — set host/port/secure/user/pass | Webhooks are not supported |
Newsletter signups
New registrants can be subscribed to a marketing newsletter list. Beehiiv is used when BEEHIIVE_API_KEY is set, otherwise Listmonk when LISTMONK_API_KEY is set; with neither set, no signup is sent.
| Variable | Default | Purpose |
|---|---|---|
BEEHIIVE_API_KEY | — | Beehiiv API key |
BEEHIIVE_PUBLICATION_ID | — | Beehiiv publication ID the subscriber is added to |
LISTMONK_API_KEY | — | Listmonk API key (HTTP Basic password, paired with LISTMONK_USER) |
LISTMONK_USER | — | Listmonk API username (HTTP Basic user) |
LISTMONK_DOMAIN | — | Base URL of the Listmonk instance. A self-hosted Listmonk on a private network must be allowlisted via SSRF_ALLOWED_PRIVATE_CIDRS |
LISTMONK_LIST_ID | — | Numeric list ID to subscribe new registrants to |
LISTMONK_WELCOME_TEMPLATE_ID | — | Numeric transactional template ID for the welcome email |
Campaign Hub
| Variable | Default | Purpose |
|---|---|---|
CAMPAIGN_PURGE_DAYS | 30 | Days after a campaign's endDate before its CampaignItem tags are purged |
Push notifications
Browser and mobile push notifications are sent via Firebase Cloud Messaging (FCM) v1. Push is globally disabled when any of these is unset.
| Variable | Default | Purpose |
|---|---|---|
FCM_PROJECT_ID | — | Firebase project ID |
FCM_CLIENT_EMAIL | — | Firebase service account client email |
FCM_PRIVATE_KEY | — | Firebase service account private key (PEM) |
Analytics & background jobs
| Variable | Default | Purpose |
|---|---|---|
USE_INNGEST | — | Set to true to enable Inngest-driven background jobs |
INNGEST_EVENT_KEY | — | Inngest Cloud event key (required unless INNGEST_DEV=1) |
INNGEST_SIGNING_KEY | — | Inngest Cloud signing key (required unless INNGEST_DEV=1) |
INNGEST_SIGNING_KEY_FALLBACK | — | Optional fallback signing key for rotation |
INNGEST_ENV | — | Optional branch environment name |
INNGEST_DEV | — | Set to 1 to use the local Inngest dev server |
INNGEST_BASE_URL | http://localhost:8288 | Local dev server URL (only used with INNGEST_DEV=1) |
INNGEST_SERVE_ORIGIN | — | Public backend origin served to Inngest |
INNGEST_SERVE_PATH | /api/inngest | Path where the backend serves the Inngest handler |
ANALYTICS_DAILY_RETENTION_DAYS | 548 | Days to keep daily channel snapshots before rolling up to weekly |
ANALYTICS_POST_RETENTION_DAYS | 90 | Days to keep per-post snapshots before pruning |
ANALYTICS_ANOMALY_Z | 3 | Z-score threshold for anomaly (spike/drop) detection in the daily sweep |
ANALYTICS_ANOMALY_COOLDOWN_DAYS | 3 | Suppress repeat anomaly notifications for the same (channel, metric) for this many days |
COMMENTS_SWEEP_INTERVAL_MINUTES | 30 | Minutes between comment collection sweeps |
POST_DAYS_BACK | 30 | Days back to look for posts when fetching comments |
SOCIAL_COMMENT_RETENTION_DAYS | 90 | Days before social comments are soft-deleted |
AGENT_DIGEST_ENABLED | — | Set to true to enable the Monday 07:00 ET headless AI digest |
AI Budget Enforcement
AI budgets are configured per-organization in Settings → AI: an organization-wide ceiling (monthly/daily caps + alert threshold, from the card at the top of Settings → AI → LLM Providers or PUT /settings/ai/budget) and per-provider caps. Once a cap is reached, AI calls are refused with HTTP 429 (org_budget_exceeded for the organization-wide ceiling). The deployment-level kill switch below controls whether the caps are actually enforced.
| Variable | Default | Purpose |
|---|---|---|
AI_PROVIDER_BUDGET_ENFORCE | true (unset = on) | When true, BudgetService.checkBudget enforces the organization-wide ceiling (Organization.aiBudget*) and the per-provider monthly/daily caps from AIOrgProviderConfig. When false, budget checks always allow the call (alerts and spend logging continue). Set to false to roll back enforcement during an incident without losing cap configuration or alerts. |
API
| Variable | Default | Purpose |
|---|---|---|
API_LIMIT | 600 | Public API rate limit per hour |
OPENAI_APP_CHALLENGE | — | Challenge string for OpenAI apps, served at /.well-known/openai-apps-challenge |
MOBILE_APP_SCHEME | postmill://auth/callback | Deep-link scheme the mobile OAuth callback (GET /auth/oauth-mobile-callback) redirects to |
NEXT_PUBLIC_OVERRIDE_BACKEND_URL | — | Overrides the token endpoint advertised in the MCP OAuth discovery document (/.well-known/oauth-authorization-server); falls back to the backend URL |
AGENT_MEDIA_SSO_KEY | — | JWT signing key for the optional agent-media.ai SSO integration. Unset = GET /user/agent-media-sso returns { url: null } |
Security
| Variable | Default | Purpose |
|---|---|---|
ENCRYPTION_KEY | (derived from JWT_SECRET) | 32-byte base64 or hex key for AES-256-GCM encryption at rest. Falls back to SHA-256 of JWT_SECRET if unset. See Security. Warning: introducing ENCRYPTION_KEY on a deployment that previously derived the key from JWT_SECRET makes all existing v2:-prefixed secrets undecryptable (single-key model) — set it from the first deploy, or plan a re-encryption of stored secrets |
INTEGRATION_RETURN_URL_ALLOWLIST | — | Comma-separated allowed partner origins for integration/enterprise return URLs |
SSRF_ALLOWED_PRIVATE_CIDRS | — | Comma-separated private CIDRs to allow for self-hosted provider instances (opt-in SSRF exception) |
RESTRICT_UPLOAD_DOMAINS | — | When set, media attached to a post must contain this domain in its path; saving a post with externally-hosted media is rejected (HTTP 400). Use the domain of your own upload endpoint |
NOT_SECURED | — | Dev-only toggle. With NODE_ENV=development it skips Helmet/HSTS and drops the Secure/sameSite flags on auth and CSRF cookies; it also suppresses the production fail-fast config exit. Never set in production |
Payments
Payment providers are enabled by their keys; billing is on when at least one is set. Details and per-provider setup: Subscriptions & payment providers.
| Variable | Default | Purpose |
|---|---|---|
PAYMENTS_PROVIDER | — | Which enabled provider serves web checkout when several are enabled (stripe, …). Unset with several enabled → error logged, Stripe preferred |
STRIPE_PUBLISHABLE_KEY | — | Stripe publishable key — enables the Stripe provider |
STRIPE_SECRET_KEY | — | Stripe secret key |
STRIPE_SIGNING_KEY | — | Stripe webhook signing secret |
STRIPE_DISCOUNT_ID | — | Stripe coupon ID. When set, eligible existing paying customers on a monthly plan (no yearly plan, no existing discount) can have the coupon applied to their subscription |
PAYPAL_CLIENT_ID | — | PayPal REST app client id — enables the PayPal provider |
PAYPAL_CLIENT_SECRET | — | PayPal REST app secret |
PAYPAL_WEBHOOK_ID | — | Id of the registered PayPal webhook (signature verification) |
PAYPAL_ENV | live | live or sandbox |
PAYPAL_BRAND_NAME | Postmill | Name shown on PayPal's approval page |
APPLE_IAP_BUNDLE_ID | — | Mobile app bundle id — enables the App Store provider |
APPLE_IAP_ISSUER_ID / APPLE_IAP_KEY_ID / APPLE_IAP_PRIVATE_KEY | — | App Store Connect In-App Purchase key (base64 .p8) |
APPLE_IAP_APP_APPLE_ID | — | Numeric Apple ID of the app |
APPLE_IAP_ENV / APPLE_IAP_ALLOW_SANDBOX | Production / — | Store environment; true also accepts sandbox purchases |
APPLE_IAP_SANDBOX_ORG_IDS | — | Comma-separated organization ids allowed to activate sandbox purchases (see Subscriptions) |
PAYMENTS_APPLE_PRODUCT_PREFIX | postmill | App Store product-id prefix |
GOOGLE_PLAY_PACKAGE_NAME | — | Mobile app package name — enables the Google Play provider |
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON | — | Base64 service-account key with Play Developer API access |
GOOGLE_PLAY_RTDN_SERVICE_ACCOUNT_EMAIL / GOOGLE_PLAY_RTDN_AUDIENCE | — | Pub/Sub push authentication (service account email; audience defaults to the webhook URL) |
PAYMENTS_GOOGLE_PRODUCT_PREFIX | postmill | Google Play product-id prefix |
ADDON_STORAGE_GB_PER_PACK | 25 | Gigabytes added by one storage add-on pack |
ADDON_VIDEO_EXPORTS_PER_PACK | 50 | Video exports added by one video-exports add-on pack |
ADDON_CHANNELS_PER_PACK | 5 | Channels added by one channels add-on pack |
ADDON_TEAM_SEATS_PER_PACK | 5 | Team seats added by one team-seats add-on pack |
ADDON_POSTS_PER_PACK | 500 | Posts per month added by one posts add-on pack |
ADDON_BRAND_KITS_PER_PACK | 5 | Brand kits added by one brand-kits add-on pack |
ADDON_WEBHOOKS_PER_PACK | 10 | Webhooks added by one webhooks add-on pack |
ADDON_COMPETITORS_PER_PACK | 10 | Competitors added by one competitors add-on pack |
ADDON_STORAGE_PRICE_CENTS | 1900 | Price per storage add-on pack, in USD cents per month |
ADDON_VIDEO_EXPORTS_PRICE_CENTS | 1900 | Price per video-exports add-on pack, in USD cents per month |
ADDON_CHANNELS_PRICE_CENTS | 1900 | Price per channels add-on pack, in USD cents per month |
ADDON_TEAM_SEATS_PRICE_CENTS | 1500 | Price per team-seats add-on pack, in USD cents per month |
ADDON_POSTS_PRICE_CENTS | 900 | Price per posts add-on pack, in USD cents per month |
ADDON_BRAND_KITS_PRICE_CENTS | 900 | Price per brand-kits add-on pack, in USD cents per month |
ADDON_WEBHOOKS_PRICE_CENTS | 900 | Price per webhooks add-on pack, in USD cents per month |
ADDON_COMPETITORS_PRICE_CENTS | 900 | Price per competitors add-on pack, in USD cents per month |
NEXT_PUBLIC_ADDON_STORAGE_GB_PER_PACK | 25 | Browser-visible mirror of ADDON_STORAGE_GB_PER_PACK |
NEXT_PUBLIC_ADDON_VIDEO_EXPORTS_PER_PACK | 50 | Browser-visible mirror of ADDON_VIDEO_EXPORTS_PER_PACK |
NEXT_PUBLIC_ADDON_CHANNELS_PER_PACK | 5 | Browser-visible mirror of ADDON_CHANNELS_PER_PACK |
NEXT_PUBLIC_ADDON_TEAM_SEATS_PER_PACK | 5 | Browser-visible mirror of ADDON_TEAM_SEATS_PER_PACK |
NEXT_PUBLIC_ADDON_POSTS_PER_PACK | 500 | Browser-visible mirror of ADDON_POSTS_PER_PACK |
NEXT_PUBLIC_ADDON_BRAND_KITS_PER_PACK | 5 | Browser-visible mirror of ADDON_BRAND_KITS_PER_PACK |
NEXT_PUBLIC_ADDON_WEBHOOKS_PER_PACK | 10 | Browser-visible mirror of ADDON_WEBHOOKS_PER_PACK |
NEXT_PUBLIC_ADDON_COMPETITORS_PER_PACK | 10 | Browser-visible mirror of ADDON_COMPETITORS_PER_PACK |
NEXT_PUBLIC_ADDON_STORAGE_PRICE_CENTS | 1900 | Browser-visible mirror of ADDON_STORAGE_PRICE_CENTS |
NEXT_PUBLIC_ADDON_VIDEO_EXPORTS_PRICE_CENTS | 1900 | Browser-visible mirror of ADDON_VIDEO_EXPORTS_PRICE_CENTS |
NEXT_PUBLIC_ADDON_CHANNELS_PRICE_CENTS | 1900 | Browser-visible mirror of ADDON_CHANNELS_PRICE_CENTS |
NEXT_PUBLIC_ADDON_TEAM_SEATS_PRICE_CENTS | 1500 | Browser-visible mirror of ADDON_TEAM_SEATS_PRICE_CENTS |
NEXT_PUBLIC_ADDON_POSTS_PRICE_CENTS | 900 | Browser-visible mirror of ADDON_POSTS_PRICE_CENTS |
NEXT_PUBLIC_ADDON_BRAND_KITS_PRICE_CENTS | 900 | Browser-visible mirror of ADDON_BRAND_KITS_PRICE_CENTS |
NEXT_PUBLIC_ADDON_WEBHOOKS_PRICE_CENTS | 900 | Browser-visible mirror of ADDON_WEBHOOKS_PRICE_CENTS |
NEXT_PUBLIC_ADDON_COMPETITORS_PRICE_CENTS | 900 | Browser-visible mirror of ADDON_COMPETITORS_PRICE_CENTS |
Build-time/runtime desync warning: the NEXT_PUBLIC_ADDON_* values are baked into the frontend bundle at build time and duplicate the backend defaults. If you change a backend ADDON_* variable, you must rebuild the frontend with matching NEXT_PUBLIC_ADDON_* values — otherwise the UI shows stale pack sizes and prices while the backend enforces the new ones. Changing an ADDON_*_PRICE_CENTS variable creates a new Stripe Price for new purchases only; existing add-on subscriptions keep billing the old price (see Subscriptions & Stripe).
Plan and add-on prices are created dynamically from pricing.ts; no STRIPE_PRICE_* IDs are read from the environment. See Subscriptions & Stripe.
SSO / OIDC login
Login providers are managed by the separate administration app (a distinct repository) and stored encrypted in AuthProviderConfig. This repo reads that config DB-first and ships no /admin frontend or login-provider write API. The variables below remain supported as the bootstrap fallback when no enabled DB config exists for that provider. Email/password (LOCAL) login is always available regardless of provider config.
See OAuth / SSO for a complete setup walkthrough.
| Variable | Default | Purpose |
|---|---|---|
POSTMILL_GENERIC_OAUTH | false | Set to true to enable generic OIDC login |
POSTMILL_OAUTH_AUTH_URL | — | OIDC provider authorization endpoint |
POSTMILL_OAUTH_TOKEN_URL | — | OIDC provider token endpoint |
POSTMILL_OAUTH_USERINFO_URL | — | OIDC provider userinfo endpoint |
POSTMILL_OAUTH_CLIENT_ID | — | OIDC client ID |
POSTMILL_OAUTH_CLIENT_SECRET | — | OIDC client secret |
POSTMILL_OAUTH_SCOPE | openid profile email | OIDC scopes to request |
NEXT_PUBLIC_POSTMILL_OAUTH_DISPLAY_NAME | — | Name shown on the login button |
NEXT_PUBLIC_POSTMILL_OAUTH_LOGO_URL | — | Logo URL shown on the login button |
Postmill ID federation
"Sign in with Postmill" lets a pinned first-party relying party (the template store) authenticate users of this instance. There is no client registration or secret, and the signing key is generated on first use — nothing needs configuring unless you change the defaults below. FRONTEND_URL must be set: it is the published authorization_endpoint.
| Variable | Default | Purpose |
|---|---|---|
FEDERATION_TRUSTED_REDIRECT_URIS | https://templates.postmill.ai/auth/callback/postmill | Comma-separated allow-list of redirect URIs an authorization code may be sent to. Exact full-string match (a trailing slash counts). Setting it replaces the default, so re-list production when adding a dev URL |
FEDERATION_ISSUER | (backend URL) | Explicit iss for issued id_tokens. Falls back to NEXT_PUBLIC_OVERRIDE_BACKEND_URL, then BACKEND_URL, then NEXT_PUBLIC_BACKEND_URL |
Social login bootstrap
These env vars are used only when no enabled DB config exists for the provider.
| Variable | Default | Purpose |
|---|---|---|
GITHUB_CLIENT_ID | — | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | — | GitHub OAuth app client secret |
YOUTUBE_CLIENT_ID | — | Google OAuth login client ID. Also the platform channel app for YouTube posting (see below); Settings → Channels remains the per-org BYO override |
YOUTUBE_CLIENT_SECRET | — | Google OAuth login client secret (dual-use, same as above) |
NEYNAR_SECRET_KEY | — | Farcaster (Neynar) login only |
Channel OAuth apps (platform click-connect)
Setting a provider's platform OAuth app credentials here gives every organization one-click Connect without per-org key entry. Leaving a provider unset requires each org to add its own app via Settings → Channels. A per-org config always takes precedence. Step-by-step portal setup for each provider: Platform Channel Apps.
Apart from the operator-level payment providers above, these channel and comms variables are the only provider credentials read from the environment. AI provider keys, short-link, and storage credentials are configured per-org in-app and have no env fallback; login-provider variables must never be used as AI credentials.
| Variable | Provider |
|---|---|
X_API_KEY / X_API_SECRET | X |
LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET | |
LINKEDIN_PAGE_CLIENT_ID / LINKEDIN_PAGE_CLIENT_SECRET | LinkedIn Page (needs a dedicated LinkedIn app — Community Management API must be its only product) |
FACEBOOK_APP_ID / FACEBOOK_APP_SECRET | Facebook (also Instagram via Facebook login) |
FACEBOOK_CONFIG_ID | Facebook Login for Business Configuration ID (optional; required for FBfB-only Meta apps) |
INSTAGRAM_APP_ID / INSTAGRAM_APP_SECRET | Instagram standalone |
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET / DISCORD_BOT_TOKEN | Discord (the bot token is also the comms platform app's) |
SLACK_ID / SLACK_SECRET | Slack |
TIKTOK_CLIENT_ID / TIKTOK_CLIENT_SECRET | TikTok |
YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET | YouTube channel posting (shared with Google login) |
PINTEREST_CLIENT_ID / PINTEREST_CLIENT_SECRET | |
REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET | |
TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET | Twitch |
THREADS_APP_ID / THREADS_APP_SECRET | Threads |
DRIBBBLE_CLIENT_ID / DRIBBBLE_CLIENT_SECRET | Dribbble |
MASTODON_CLIENT_ID / MASTODON_CLIENT_SECRET | Mastodon (optional — tenants can also just enter their instance hostname) |
MEWE_APP_ID / MEWE_API_KEY | Mewe |
KICK_CLIENT_ID / KICK_SECRET | Kick |
GOOGLE_GMB_CLIENT_ID / GOOGLE_GMB_CLIENT_SECRET | Google Business Profile |
NEYNAR_CLIENT_ID / NEYNAR_SECRET_KEY | Farcaster (Wrapcast) channel posting |
VK_ID | VK (id only, no secret) |
WHOP_CLIENT_ID | Whop (id only, PKCE) |
TELEGRAM_TOKEN | Telegram bot token (token-only) |
LINE_CHANNEL_ACCESS_TOKEN | LINE Messaging API channel access token (token-only) |
SLACK_SIGNING_SECRET | Slack comms platform app — webhook signing secret (comms only; see Platform Comms Apps) |
DISCORD_PUBLIC_KEY | Discord comms platform app — interaction webhook verification (comms only; see Platform Comms Apps) |
LINE_CHANNEL_SECRET | LINE comms platform app — webhook signature verification (comms only; see Platform Comms Apps) |
Three opt-in flags add the platform channel apps as login providers (the login page never advertises a provider whose credentials are incomplete). Facebook and LinkedIn reuse the channel creds above; X login needs its own OAuth 2.0 client pair — the OAuth 1.0a X_API_KEY / X_API_SECRET are channel-only. See Platform Channel Apps → SSO dual-use. Sign in with Apple is a separate, login-only provider with its own credentials (last rows below).
| Variable | Default | Purpose |
|---|---|---|
FACEBOOK_SSO_ENABLED | false | Set to true to add a "Continue with Facebook" login button (requires FACEBOOK_APP_ID / FACEBOOK_APP_SECRET) |
FACEBOOK_SSO_CONFIG_ID | — | Facebook Login for Business Configuration ID used for login on Business-type Meta apps (login-only Configuration, "User access token" variation). Unset = falls back to the channel FACEBOOK_CONFIG_ID; with neither, the classic scope=public_profile,email dialog is used (Consumer-type apps) |
X_SSO_ENABLED | false | Set to true to add a "Continue with X" login button (requires X_CLIENT_ID / X_CLIENT_SECRET) |
X_CLIENT_ID / X_CLIENT_SECRET | — | The X app's OAuth 2.0 Client ID and Client Secret (Keys and tokens; issued once OAuth 2.0 is enabled under User authentication settings). Login only — never used for posting. Login requests tweet.read users.read users.email; enable "Request email from users" on the app for the real address |
LINKEDIN_SSO_ENABLED | false | Set to true to add a "Continue with LinkedIn" login button (requires LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET) |
APPLE_SSO_ENABLED | false | Set to true to add Sign in with Apple (login only — no channel app). Requires the four APPLE_* values below; see OAuth / SSO → Apple login |
APPLE_CLIENT_ID / APPLE_TEAM_ID / APPLE_KEY_ID / APPLE_PRIVATE_KEY | — | Apple Services ID, Developer Team ID, Sign in with Apple key ID, and the base64-encoded .p8 key |
X channel behaviour
| Variable | Default | Purpose |
|---|---|---|
DISABLE_X_ANALYTICS | — | Set to skip X (Twitter) analytics collection |
X_ANALYTICS_MAX_PAGE_DEPTH | 10 | Maximum timeline pages fetched per X analytics sync |
STRIP_LINKS_FROM_X_POSTS | — | Set to strip links from X posts before publishing |
Browser extension
| Variable | Default | Purpose |
|---|---|---|
EXTENSION_ID | — | Chrome extension ID for cookie-based platform integrations (Skool) |
Monitoring
| Variable | Default | Purpose |
|---|---|---|
NEXT_PUBLIC_SENTRY_DSN | — | Sentry DSN for error tracking (frontend) |
SENTRY_SPOTLIGHT | — | Set to 1 to enable Spotlight debug proxy |
DATAFAST_API_KEY | — | Datafast API key. Posts a register goal to datafa.st when a new user signs up carrying a datafast_visitor_id |
FACEBOOK_PIXEL_ACCESS_TOKEN | — | Meta Conversions API access token for server-side pixel events |
NEXT_PUBLIC_FACEBOOK_PIXEL | — | Meta pixel ID (browser-visible). Server-side pixel events fire only when both this and FACEBOOK_PIXEL_ACCESS_TOKEN are set |
NEXT_PUBLIC_PLAUSIBLE_DOMAIN | — | Plausible-registered domain for the frontend analytics tracker. Unset = tracker off (legacy fallback: postmill.ai when billing is enabled) |
Provider errors vs. Postmill errors. When an organization's own AI / media provider (OpenAI, Gemini, Runway, …) rejects a request — bad key, quota or billing limit, rate limit, invalid request, outage — the API answers 502 with { error: "ProviderUpstreamError", provider, providerName, kind, upstreamStatus, message, settingsUrl } and the UI shows the provider's name and message ("Google AI Studio reports the account's quota or billing limit was reached (HTTP 429): …"). kind is one of auth, quota, rate_limit, invalid_request, timeout, unavailable, unknown. The backend logs one provider upstream error <domain>/<provider> <kind> HTTP <status> warning per occurrence. Sentry receives onlyunavailable / timeout / unknown kinds, as warnings tagged provider + kind (provider outages are worth seeing; a user's expired key or exhausted plan is not an application error). A 500 from the API therefore always means a Postmill-side failure.
AI Designer chatbot
| Variable | Default | Purpose |
|---|---|---|
AI_DESIGNER_MESH_STORE | redis | Agent-mesh session/breaker store: redis (default) or postgres (opt-in, runs its own DDL) |
AI_DESIGNER_MESH_DATABASE_URL | — | Dedicated Postgres URL for the opt-in mesh Postgres store. Never the Prisma DATABASE_URL |
AI_DESIGNER_MESH_CONNECTION_LIMIT | 10 | Connection pool size for the opt-in mesh Postgres store |
AI_DESIGNER_AGENT_REGISTRY | — | Directory of per-agent YAML files overriding the bundled registry |
AI_DESIGNER_AGENT_TIMEOUT_MS | 120000 | Per-agent LLM dispatch deadline |
AI_DESIGNER_ASSET_TIMEOUT_MS | 90000 | Asset (image generation/stock) step deadline |
AI_DESIGNER_SCENE_VIDEO_TIMEOUT_MS | 480000 | Deadline for one generated scene video in a video session |
AI_DESIGNER_STUCK_SESSION_MINUTES | 15 | Planning/executing sessions untouched longer than this roll back to awaiting_plan |
TRUST_PROXY_HOPS | (disabled) | Number of XFF-appending reverse proxies in front of the backend. Unset/invalid = key on the socket peer address (safe default, not spoofable). When set, per-IP rate buckets key on the Nth-from-right x-forwarded-for entry. Applies to the /ai-designer connect-rate bucket and the HTTP throttler's per-IP buckets (login/register/enterprise/public-report) and the MCP rate limits. Operators behind a proxy must set it to the exact number of appending proxies — overestimating lands in attacker-controlled left-most XFF entries and makes the limits spoofable |
SESSION_TTL_MINUTES | — | agent-mesh package env var — no Postmill effect (see caveat below) |
ENABLE_CIRCUIT_BREAKER | — | agent-mesh package env var — forced off (see caveat below) |
MCP_MAX_RETRIES | — | agent-mesh package env var — no Postmill effect (see caveat below) |
SESSION_TTL_MINUTES, ENABLE_CIRCUIT_BREAKER, and MCP_MAX_RETRIES belong to the bundled @reaatech/agent-mesh package's env schema, not to Postmill itself. agent-mesh-env.stash.ts reads, neutralizes, and restores them around the package's import-time env parse: out-of-range values are stashed aside so they cannot crash the boot, and ENABLE_CIRCUIT_BREAKER is forced off because the package's global breaker is keyed by agent id only — Postmill uses the conductor's per-(org, agent) breaker instead. Setting these vars does not configure Postmill behaviour.
Local development feature flags
Set any of these to true or 1 to disable the corresponding subsystem during local development. All features remain enabled by default.
| Variable | Purpose |
|---|---|
DEV_DISABLE_AI | Skip AI adapter registration |
DEV_DISABLE_MCP | Skip Mastra/MCP/A2A server startup |
DEV_DISABLE_MEDIA | Skip media-generation adapter registration |
DEV_DISABLE_SHORTLINKS | Skip short-link adapter registration |
DEV_DISABLE_EMAIL | Skip email-provider adapter registration |
DEV_DISABLE_VIDEO | Skip video-generation adapter registration |
DEV_DISABLE_AGENT | Skip agent-graph services |
DEV_DISABLE_CRON | Skip ScheduleModule.forRoot() |
DEV_DISABLE_SENTRY | Skip Sentry initialization |
DEV_DISABLE_OPENTELEMETRY | Skip OpenTelemetry exporter setup |
DEV_DISABLE_COMMS | Skip comms (chat-app) adapter registration |
AGENT_SUPERVISOR_ENABLED | true (default) uses the supervisor + specialists agent model |
CONTENT_PIPELINE_TOTAL_TIMEOUT_MS | 300000 |
CONTENT_PIPELINE_AGENT_TIMEOUT_MS | 120000 |
BACKEND_URL | Server-side backend URL used by the MCP surface. Falls back to NEXT_PUBLIC_BACKEND_URL |
MEDIA_MCP_AUDIT_LOG_PATH | /tmp/media-mcp-audit.log |
SENTRY_PROFILING | Set to 1 to enable browser profiling in dev |
FRONTEND_PROFILING | Set to 1 to enable Document-Policy: js-profiling in dev |
DEV_SEED_DEMO | Populate the target org with placeholder demo data at backend boot (dev only) |
DEV_SEED_DEMO_RESET | Wipe and reseed demo data (dev only) |
DEV_SEED_DEMO_EMAIL | Demo seeder account email |
DEV_SEED_DEMO_PASSWORD | Demo seeder account password |
Production, scaling & observability
| Variable | Default | Purpose |
|---|---|---|
CONFIG_CHECK_STRICT | — | Fail fast on fatal-missing secrets even in dev |
BACKEND_LISTEN_HOST | (Node default) | Explicit bind host for the backend HTTP server (e.g. 0.0.0.0 to force IPv4 in CI/containers). Unset = Node's default bind address |
COLLAB_SINGLE_INSTANCE | true | Collaboration websocket keeps Yjs state in memory; must be true unless COLLAB_REDIS_ADAPTER is set |
COLLAB_REDIS_ADAPTER | — | Reserved for the future Yjs-over-Redis adapter |
OUTBOUND_HTTP_TIMEOUT_MS | 30000 | Bound provider and webhook calls |
WEBHOOK_TIMEOUT_MS | 10000 | Outbound webhook delivery timeout |
WEBHOOK_SIGNING_SECRET | — | HMAC secret for X-Postmill-Signature. When unset, derives from JWT_SECRET |
OTEL_EXPORTER_OTLP_ENDPOINT | — | OTLP/HTTP tracing endpoint. Off unless set |
OTEL_SERVICE_NAME | postmill-backend | OpenTelemetry service name |
DATABASE_CONNECTION_LIMIT | — | Prisma connection pool size appended to DATABASE_URL |
DATABASE_POOL_TIMEOUT | — | Prisma pool timeout appended to DATABASE_URL |
ALLOW_DESTRUCTIVE_SCHEMA | false | Allow prisma db push to perform destructive diffs |
ERRORS_RETENTION_DAYS | 90 | Retention for Errors rows |
NOTIFICATIONS_RETENTION_DAYS | 180 | Retention for Notifications (+ NotificationRead) |
MULTIPART_UPLOAD_RETENTION_DAYS | 7 | Retention for abandoned multipart uploads |
MASTRA_TRACE_RETENTION_DAYS | 30 | Retention for Mastra traces/scorers |
SOFT_DELETE_RETENTION_DAYS | 30 | Hard-purge window for soft-deleted posts/files |
BLANK_DESIGN_RETENTION_DAYS | 1 | Days an untouched, content-free Untitled Design is kept before the retention sweep soft-deletes it (with its preview file) |
IP_RETENTION_DAYS | 90 | Null User/Session IP and agent after this window |
AI_DESIGNER_SESSION_RETENTION_DAYS | 90 | Retention for AI Designer chat sessions |
DESIGN_VERSIONS_KEEP_AUTO | 10 | Automatic version snapshots kept per Designer design (oldest trimmed on insert) |
DESIGN_VERSIONS_KEEP_NAMED | 20 | Named version snapshots kept per Designer design (oldest trimmed on insert) |
Stock media
| Variable | Default | Purpose |
|---|---|---|
UNSPLASH_ACCESS_KEY | — | Unsplash API access key for stock photos |
PEXELS_API_KEY | — | Pexels API key for stock videos |
PIXABAY_API_KEY | — | Pixabay API key for vectors/illustrations |
GIPHY_API_KEY | — | GIPHY API key for stickers |
JAMENDO_CLIENT_ID | — | Jamendo API client ID for stock audio |
Iconify (SVG icons) does not require an API key.
Content Packs
Premium stock sources are configured per-organization in-app via Settings → Content Packs. BYOK packs take precedence over the free catalogs above for the capabilities they support. See Storage and the user-facing settings docs.
Video rendering
See Video Rendering for the full list of VIDEO_RENDER_* variables.
Headless Chromium (Puppeteer)
| Variable | Default | Purpose |
|---|---|---|
PUPPETEER_EXECUTABLE_PATH | (bundled Chromium) | Path to a system/distro Chromium binary for design frame capture (used by the render-worker container). Unset = Puppeteer's bundled Chromium |
PUPPETEER_DISABLE_SANDBOX | false | Set to true to launch the campaign-report PDF Chromium with --no-sandbox (for CI/containers without user namespaces; also disabled automatically when CI=true). Production defaults to sandboxed mode |
Verified against v1.3.0