Multica Docs

Environment variables

The full list of environment variables for running a self-hosted Multica server.

A self-hosted Multica server reads its configuration from environment variables at startup — database, sign-in, email, storage, signup allowlists all live here. This page groups every variable by purpose: each section spells out what happens if you leave it unset and which ones you must set in production. For how to actually configure the auth-related ones, see Sign-in and signup configuration.

Core server variables

These are the core variables you must think about before deploying — some have defaults that let the server start, but in production you should set the required ones explicitly.

VariableDefaultRequired in production?
DATABASE_URLpostgres://multica:multica@localhost:5432/multica?sslmode=disableYes
PORT8080No (unless you change the port)
JWT_SECRETmultica-dev-secret-change-in-productionYes (the default is unsafe)
APP_ENVemptyYes (must be production)
FRONTEND_ORIGINemptyYes (self-host must set its own domain)
MULTICA_DEV_VERIFICATION_CODEemptyNo (must stay empty in production)

Keep MULTICA_DEV_VERIFICATION_CODE empty in production. A fixed local test code is disabled by default, but if you opt in with MULTICA_DEV_VERIFICATION_CODE=888888, anyone who can request a code can sign in with that fixed value while APP_ENV is non-production. The shortcut is ignored when APP_ENV=production.

Database connection pool

VariableDefaultDescription
DATABASE_MAX_CONNS25pgxpool max connections. The daemon polls frequently (every 3s) and uses connections; larger deployments may need a higher value
DATABASE_MIN_CONNS5Minimum idle connections

When unset, the values above are used — not pgx's built-in 4/NumCPU defaults, which previously caused pool exhaustion in production.

Email configuration

Multica supports two delivery backends — Resend for cloud deployments, or an SMTP relay for internal / on-premise networks. SMTP_HOST takes priority over RESEND_API_KEY when both are set.

Resend

VariableDefaultDescription
RESEND_API_KEYemptyResend API key
RESEND_FROM_EMAILnoreply@shayanlatif.comSender address (must be a domain verified in your Resend account; also reused as the From: header when SMTP is in use)

SMTP relay

VariableDefaultDescription
SMTP_HOSTemptySMTP relay hostname. Setting this activates SMTP mode and overrides Resend
SMTP_PORT25SMTP port. Use 587 for STARTTLS submission; port 465 (SMTPS / implicit TLS) is not supported
SMTP_USERNAMEemptySMTP username. Leave empty for unauthenticated relay
SMTP_PASSWORDemptySMTP password
SMTP_TLS_INSECUREfalseSet true to skip TLS certificate verification (private CA / self-signed only)

STARTTLS is upgraded automatically when the server advertises it. The dial timeout is 10s and the whole SMTP session has a 30s deadline, so a black-holed relay can't hang the auth handler.

Behavior when neither is set: the server does not error, but every email that should have been sent (verification codes, invite links) is written to the server's stdout only. Convenient for local development — copy the code out of the server logs; in production, forgetting to set this creates a silent black hole, with users never receiving email and no error surfaced.

Google OAuth configuration

Optional. Leave unset for email + verification code only; configure it to add "Sign in with Google" on the sign-in page.

VariableDefaultDescription
GOOGLE_CLIENT_IDemptyGoogle Cloud OAuth client ID
GOOGLE_CLIENT_SECRETemptyGoogle Cloud OAuth secret
GOOGLE_REDIRECT_URIhttp://localhost:3000/auth/callbackOAuth callback URL (self-host: replace with your frontend domain)

Takes effect at runtime: the frontend reads these settings via /api/config at runtime, so changing them requires no frontend rebuild or redeploy — restart the server and they apply.

Full setup (including Google Cloud Console steps) is in Sign-in and signup configuration.

File storage configuration

Multica stores user-uploaded attachments (images and files in comments). S3 is preferred; if S3 is not configured, it falls back to local disk.

S3 / S3-compatible storage

VariableDefaultDescription
S3_BUCKETemptyBucket name only (for example my-bucket). Do not include the .s3.<region>.amazonaws.com suffix — the server constructs the public host from S3_BUCKET + S3_REGION. Setting this enables S3 storage
S3_REGIONus-west-2AWS region. Must match the bucket's actual region — it is used both for SDK signing and for building the public URL
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYemptyStatic credentials. When both are unset, the AWS SDK default credential chain is used (IAM role / environment credentials)
AWS_ENDPOINT_URLemptyCustom S3-compatible endpoint (for example MinIO). Setting this switches to path-style URLs

When S3_BUCKET is unset: the server logs "S3_BUCKET not set, cloud upload disabled" at startup, and all uploads fall back to local disk.

Public URLs are constructed in this order of priority:

  1. https://<CLOUDFRONT_DOMAIN>/<key> if CLOUDFRONT_DOMAIN is set.
  2. <AWS_ENDPOINT_URL>/<S3_BUCKET>/<key> (path-style) if AWS_ENDPOINT_URL is set.
  3. https://<S3_BUCKET>.s3.<S3_REGION>.amazonaws.com/<key> (virtual-hosted-style). When S3_BUCKET contains dots, the server falls back to https://s3.<S3_REGION>.amazonaws.com/<S3_BUCKET>/<key> (path-style) because the AWS-issued wildcard TLS certificate does not validate dotted bucket hosts.

Local disk (when S3 is not configured)

VariableDefaultDescription
LOCAL_UPLOAD_DIR./data/uploadsLocal storage directory
LOCAL_UPLOAD_BASE_URLempty (returns relative paths)Public base URL — leave unset and the frontend can't resolve a full URL for attachments

CloudFront (optional)

If you front S3 with CloudFront, three variables apply: CLOUDFRONT_DOMAIN, CLOUDFRONT_KEY_PAIR_ID, CLOUDFRONT_PRIVATE_KEY (or CLOUDFRONT_PRIVATE_KEY_SECRET to read from Secrets Manager). Skip them if you don't use CloudFront — they don't conflict with S3 configuration.

VariableDefaultDescription
COOKIE_DOMAINemptyScope of the session cookie
  • Empty: the cookie is valid only on the exact host visited (correct for single-host deployments)
  • Set to .example.com: the cookie is shared across subdomains (so app.example.com and api.example.com share a sign-in session)
  • Warning: it cannot be an IP address (browsers ignore it)

Restricting who can sign up

Three allowlist layers combine by priority. If any layer is set to a non-empty value, emails that don't match are rejected — even ALLOW_SIGNUP=true won't override that.

VariableDefaultDescription
ALLOWED_EMAILSemptyExplicit email allowlist (comma-separated). When non-empty, only listed emails can sign up
ALLOWED_EMAIL_DOMAINSemptyDomain allowlist (comma-separated). When non-empty, only listed domains can sign up
ALLOW_SIGNUPtrueSignup master switch. Set false to disable signup entirely

The counterintuitive part: ALLOWED_EMAIL_DOMAINS=company.io + ALLOW_SIGNUP=true does not mean "allow company.io or everyone" — it means only allow company.io. The allowlist layers are AND semantics — the full decision tree is in Sign-in and signup configuration → Signup allowlists.

Invite flows themselves do not check the signup allowlist — but the invitee must still be able to sign in before accepting the invite. If they already have a Multica account (for example from another workspace), they can accept directly, unaffected by the allowlist; if they have never signed up, the first step of sign-in (requesting a verification code) still passes through the allowlist check, and an email rejected by ALLOW_SIGNUP=false or by ALLOWED_EMAILS / ALLOWED_EMAIL_DOMAINS cannot finish signup, and therefore cannot accept the invite.

Rate limiting (optional Redis)

Public auth endpoints — /auth/send-code, /auth/verify-code, /auth/google — have per-IP fixed-window rate limiting in front of them. The limiter is backed by Redis. When REDIS_URL is unset the middleware is a no-op (fail-open) and the backend logs rate limiting disabled: REDIS_URL not configured at startup.

VariableDefaultDescription
REDIS_URLemptyRedis connection URL (for example redis://localhost:6379/0). When unset, rate limiting on auth endpoints is disabled. The same Redis is also used by the realtime hub fan-out, the PAT cache, and the daemon-token cache — they all fall back to in-memory / direct-DB mode when unset
RATE_LIMIT_AUTH5Max requests per IP per minute against /auth/send-code and /auth/google
RATE_LIMIT_AUTH_VERIFY20Max requests per IP per minute against /auth/verify-code
RATE_LIMIT_TRUSTED_PROXIESemptyComma-separated CIDRs whose X-Forwarded-For header the limiter is allowed to trust. Empty (the default) means never trust XFF — the limiter only uses the direct connection's RemoteAddr

When a request is over the limit, the server replies with 429 Too Many Requests, Retry-After: 60, and body {"error":"too many requests"}.

Behind a reverse proxy you must set RATE_LIMIT_TRUSTED_PROXIES. Otherwise every real user shares the proxy's IP from the backend's point of view, the whole deployment ends up in one bucket, and /auth/send-code becomes 5 req/min for the entire site. Typical values: 127.0.0.1/32,::1/128 for a same-host Caddy / Nginx; the CDN's published ranges for Cloudflare / ALB / CloudFront. Only IPs whose RemoteAddr falls inside one of these CIDRs may use X-Forwarded-For to identify the client.

This separate RATE_LIMIT_TRUSTED_PROXIES is not the same as MULTICA_TRUSTED_PROXIES, which controls the autopilot-webhook limiter (/api/webhooks/autopilots/{token}). Each limiter parses its own list, so a deployment behind a proxy should set both.

Daemon tuning parameters

The daemon runs on the user's local machine, and its config is read from local environment variables too. The common ones:

VariableDefaultDescription
MULTICA_SERVER_URLws://localhost:8080/wsServer address (self-host: replace with your domain)
MULTICA_DAEMON_HEARTBEAT_INTERVAL15sHeartbeat interval
MULTICA_DAEMON_POLL_INTERVAL3sTask polling interval
MULTICA_DAEMON_MAX_CONCURRENT_TASKS20Max concurrent tasks
MULTICA_<PROVIDER>_PATHmatches the CLI namePath to each AI coding tool's executable (for example MULTICA_CLAUDE_PATH)
MULTICA_<PROVIDER>_MODELemptyDefault model for each AI coding tool

For a full explanation of how each parameter affects daemon behavior, see Daemon and runtimes.

Frontend access control

VariableDefaultDescription
FRONTEND_ORIGINemptyFrontend address. Invite email links, the CORS allowlist, and the cookie domain are all derived from this. When unset, invite email links fall back to the hosted domain https://app.multica.shayanlatif.com — self-host must set this explicitly
CORS_ALLOWED_ORIGINSemptyAdditional allowed CORS origins (comma-separated)
ALLOWED_ORIGINSemptyWebSocket-specific origin allowlist (comma-separated); when unset, fallback order is CORS_ALLOWED_ORIGINSFRONTEND_ORIGINlocalhost:3000/5173/5174

Leaving FRONTEND_ORIGIN unset creates two silent failures: (1) invite email links point at https://app.multica.shayanlatif.com (the hosted domain), and clicking them doesn't bring users back to your self-hosted instance; (2) WebSocket Origin checks fall back to localhost:3000 / 5173 / 5174, so every WebSocket connection in a production deployment is rejected and the frontend appears to "lose real-time updates."

GitHub integration

The GitHub PR ↔ issue integration needs two variables. Set both to enable Connect GitHub in Settings and accept incoming webhooks.

VariableDefaultDescription
GITHUB_APP_SLUGemptyThe slug of your GitHub App (the tail of https://github.com/apps/<slug>). Drives the Settings → GitHub install button URL
GITHUB_WEBHOOK_SECRETemptyThe Webhook secret you set on the GitHub App. Used for HMAC-SHA256 verification of every pull_request / installation delivery, and as the HMAC key for the setup-callback state token

Behavior when either is unset:

  • Connect GitHub in Settings → GitHub is disabled and shows a "not configured" hint to admins.
  • The /api/webhooks/github endpoint returns 503 github webhooks not configured — Multica refuses to process events with no secret rather than treating every signature as valid.

Note: GITHUB_WEBHOOK_SECRET is reused as the signing key for the install-flow state token, so operators only need to manage one secret. It is not the GitHub App's Client secret — Client secrets are OAuth-related and not used by this integration. See GitHub integration → Self-host setup for the full walkthrough.

Usage analytics

By default, the server reports to Multica's official PostHog instance. To opt out, set ANALYTICS_DISABLED=true.

VariableDefaultDescription
ANALYTICS_DISABLEDfalseSet true to disable backend analytics entirely
POSTHOG_API_KEYbuilt-in default keySet when pointing at your own PostHog instance
POSTHOG_HOSThttps://us.i.posthog.comChange to your own host if you self-host PostHog

Next