Environment variables
Common server, storage, integration, and runtime configuration for self-hosting Multica.
Multica reads environment variables at process startup. After changing one, restart the affected API, web, or daemon process. Docker Compose's docker compose restart does not re-read .env; recreate containers with up -d for changes to take effect.
This page lists deployment-facing configuration only; test variables and internal task variables are not covered here. It is a grouped reference — for full deployment steps, see the self-host quickstart.
Minimum production configuration
DATABASE_URL=postgres://user:password@postgres:5432/multica?sslmode=require
JWT_SECRET=<long-random-secret>
APP_ENV=production
FRONTEND_ORIGIN=https://multica.example.com
MULTICA_APP_URL=https://multica.example.com
MULTICA_PUBLIC_URL=https://api.multica.example.comYou also need to pick an email service, otherwise verification codes and invitations are only written to the server log.
Do not use the default JWT_SECRET in production, and do not set MULTICA_DEV_VERIFICATION_CODE.
API and database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | local multica database | PostgreSQL connection string |
DATABASE_MAX_CONNS | 25 | Maximum database connections per API process |
DATABASE_MIN_CONNS | 5 | Minimum connections each API process keeps |
PORT | 8080 | API listen port |
JWT_SECRET | fixed development value | Secret for sign-in JWTs and some signing flows |
APP_ENV | empty | Set to production in production |
AUTH_TOKEN_TTL | 720h (30 days) | Lifetime of browser JWTs and cookies; accepts a Go duration or a positive integer of seconds |
LOG_LEVEL | application default | Log level |
MULTICA_SHUTDOWN_HOLD_DURATION | 0 | How long to wait after a termination signal before graceful shutdown begins |
When setting a shutdown hold on Kubernetes, terminationGracePeriodSeconds must exceed the hold plus the time the actual shutdown needs.
Public URLs and browser access
| Variable | Default | Description |
|---|---|---|
FRONTEND_ORIGIN | empty | Frontend origin users visit; used for CORS, cookies, and invitation links |
MULTICA_APP_URL | falls back to FRONTEND_ORIGIN | User-reachable web URL; CLI sign-in and account-link URLs use it |
MULTICA_PUBLIC_URL | empty | Public API URL; used for webhook URLs and runtime connection instructions |
CORS_ALLOWED_ORIGINS | empty | Extra allowed HTTP origins, comma-separated |
ALLOWED_ORIGINS | falls back to CORS / frontend origins | WebSocket origin allowlist, comma-separated |
COOKIE_DOMAIN | empty | Required when the frontend and API use different hosts and the browser talks to the API domain directly; keep empty for single-domain deployments |
When the frontend and API run on different hosts and the browser talks to the API domain directly, you must set COOKIE_DOMAIN — otherwise the browser cannot read the CSRF cookie: every write request returns 403 CSRF validation failed while reads work fine. Use the narrowest parent domain that covers both hosts (.agent.example.com over .example.com). It spreads the sign-in session cookie to every host under that domain, which is only acceptable when all of those hosts are operated by the same trusted party. After changing it, clear the old cookies on both hosts and sign in again. If you follow the same-origin recipe in the self-host quickstart (the browser only visits the app domain), keep it empty. Do not use an IP address — browsers ignore cookies whose Domain is an IP.
Self-hosted deployments must set FRONTEND_ORIGIN. Without it, invitation links, cookie security attributes, and WebSocket origin checks can all disagree with your actual domain.
Email and sign-in
Resend
| Variable | Default | Description |
|---|---|---|
RESEND_API_KEY | empty | Setting it enables Resend |
RESEND_FROM_EMAIL | noreply@multica.ai | Sender address; must belong to a verified domain |
SMTP
SMTP takes priority over Resend whenever SMTP_HOST is non-empty.
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | empty | SMTP host; setting it enables SMTP |
SMTP_PORT | 25 | Common values: 25, 587, 465 |
SMTP_USERNAME | empty | Username; leave empty for anonymous relays |
SMTP_PASSWORD | empty | Password |
SMTP_FROM_EMAIL | falls back to RESEND_FROM_EMAIL | Envelope From and message From |
SMTP_TLS | starttls | implicit, smtps, or ssl means implicit TLS; port 465 enables it automatically |
SMTP_TLS_INSECURE | false | Skips certificate verification; trusted internal networks only |
SMTP_EHLO_NAME | hostname | EHLO/FQDN required by strict relays |
Google OAuth
| Variable | Default | Description |
|---|---|---|
GOOGLE_CLIENT_ID | empty | Google OAuth client ID |
GOOGLE_CLIENT_SECRET | empty | Google OAuth client secret |
GOOGLE_REDIRECT_URI | http://localhost:3000/auth/callback | Must exactly match the callback URL in the Google Console |
Signup scope
| Variable | Default | Description |
|---|---|---|
ALLOW_SIGNUP | true | Whether new accounts can be created when no allowlist is configured |
ALLOWED_EMAILS | empty | Full email addresses allowed to sign up, comma-separated |
ALLOWED_EMAIL_DOMAINS | empty | Email domains allowed to sign up, comma-separated |
DISABLE_WORKSPACE_CREATION | false | Blocks all users from creating workspaces; no owner/admin exception |
MULTICA_DEV_VERIFICATION_CODE | empty | Fixed 6-digit test code for non-production environments |
For the exact allowlist evaluation order, see Sign-in and signup.
Attachment storage
When S3_BUCKET is unset, Multica uses local disk.
S3 or compatible storage
| Variable | Default | Description |
|---|---|---|
S3_BUCKET | empty | Bucket name; do not use the full hostname |
S3_REGION | us-west-2 | Region the bucket lives in |
AWS_ACCESS_KEY_ID | SDK default credential chain | Static access key |
AWS_SECRET_ACCESS_KEY | SDK default credential chain | Static secret key |
AWS_ENDPOINT_URL | empty | S3-compatible endpoint such as MinIO |
S3_USE_PATH_STYLE | true with a custom endpoint | Whether to use path-style addressing |
ATTACHMENT_DOWNLOAD_MODE | auto | auto, cloudfront, presign, or proxy |
ATTACHMENT_DOWNLOAD_URL_TTL | 30m | Lifetime of signed download URLs |
Use ATTACHMENT_DOWNLOAD_MODE=proxy when the endpoint — an internal MinIO, for example — is not reachable from the browser.
Local disk
| Variable | Default | Description |
|---|---|---|
LOCAL_UPLOAD_DIR | ./data/uploads | Directory for files and metadata; needs a persistent volume |
LOCAL_UPLOAD_BASE_URL | empty | Optional public base URL; when empty, in-app relative URLs are returned |
CloudFront
| Variable | Description |
|---|---|
CLOUDFRONT_DOMAIN | CDN domain |
CLOUDFRONT_KEY_PAIR_ID | CloudFront key pair ID |
CLOUDFRONT_PRIVATE_KEY | Full private key |
CLOUDFRONT_PRIVATE_KEY_SECRET | Use when reading the private key from Secrets Manager |
Redis and rate limiting
| Variable | Default | Description |
|---|---|---|
REDIS_URL | empty | Used for shared rate limiting, realtime events, and the token cache; when unset, realtime events and invitation limits fall back to in-process memory, while auth rate limiting is off |
REDIS_DISABLE_CLIENT_NAME | false | Set true when a managed Redis blocks CLIENT SETNAME |
RATE_LIMIT_AUTH | 5 | Per-IP requests per minute to send a verification code or start Google sign-in |
RATE_LIMIT_AUTH_VERIFY | 20 | Per-IP code verifications per minute |
RATE_LIMIT_INVITATION_ACTOR_10M | 10 | Workspace invitations each inviter may create per 10-minute sliding window; 0 disables this gate |
RATE_LIMIT_INVITATION_WORKSPACE_24H | 50 | Workspace invitations all admins may create per workspace per 24-hour sliding window; 0 disables this gate |
RATE_LIMIT_INVITATION_RECIPIENT_24H | 6 | Invitations one normalized recipient email may receive across workspaces per 24-hour sliding window; 0 disables this gate |
RATE_LIMIT_TRUSTED_PROXIES | empty | Comma-separated proxy CIDRs allowed to provide X-Forwarded-For |
MULTICA_TRUSTED_PROXIES | empty | Trusted proxy CIDRs for automation webhooks and realtime connections |
Deployments behind a reverse proxy must list their real proxy ranges. Do not blanket-trust all sources, or clients can forge forwarded IPs.
The auth rate limits require REDIS_URL; without it, the startup log notes that auth rate limiting is disabled. Invitation limits still run in process-local memory without Redis and become shared across replicas when Redis is configured. If configured Redis becomes temporarily unavailable, auth rate limiting fails open, while invitation creation returns a retryable 503 instead of sending email without protection.
External integrations
| Integration | Variable | Description |
|---|---|---|
| GitHub | GITHUB_APP_SLUG | GitHub App slug |
| GitHub | GITHUB_WEBHOOK_SECRET | Webhook HMAC and connect-state signing secret |
| GitHub | GITHUB_APP_ID | Needed for CI status and mergeability on PR cards and the "pick from GitHub" repository picker |
| GitHub | GITHUB_APP_PRIVATE_KEY | Full PEM private key paired with the App ID; same uses as above |
| Lark | MULTICA_LARK_SECRET_KEY | Base64-encoded 32-byte credential encryption key |
| Slack | MULTICA_SLACK_SECRET_KEY | Base64-encoded 32-byte token encryption key |
| Composio | COMPOSIO_API_KEY | Enables Composio tool connections |
| Composio | COMPOSIO_CALLBACK_BASE_URL | Callback API URL; can fall back to MULTICA_PUBLIC_URL |
| Composio | COMPOSIO_STATE_SECRET | OAuth state signing secret; can be derived from JWT_SECRET |
| Self-hosted Git | MULTICA_VCS_INTEGRATION_ENABLED | Forgejo/Gitea/GitLab integration switch; enabled by default in compose |
| Self-hosted Git | MULTICA_VCS_SECRET_KEY | Base64-encoded 32-byte encryption key (openssl rand -base64 32); the feature is entirely unavailable without it |
Without GITHUB_APP_ID and the private key, PRs still link, mirror, and trigger merge-to-done normally, but cards show no CI or mergeability status, and the "pick from GitHub" repository entry is disabled.
For setup steps, see GitHub integration, Lark bot, and Slack bot.
Server-side LLM
This group configures server-side assist generation, such as conversation titles; it is not the AI coding tool credentials agents use to execute tasks.
| Variable | Default | Description |
|---|---|---|
MULTICA_LLM_API_KEY | empty | OpenAI-compatible API key |
MULTICA_LLM_BASE_URL | empty | OpenAI-compatible endpoint |
MULTICA_LLM_DEFAULT_MODEL | gpt-5.6-luna | Used when a request does not specify a model |
When both the API key and base URL are empty, server-side LLM generation is off and callers use local fallback logic.
Daemon configuration
The variables below are read on the computer that runs your agents, not in the API container.
| Variable | Default | Description |
|---|---|---|
MULTICA_SERVER_URL | ws://localhost:8080/ws | Multica API / WebSocket URL; also accepts http(s) |
MULTICA_DAEMON_DEVICE_NAME | hostname | Device name shown in the runtime list |
MULTICA_AGENT_RUNTIME_NAME | Local Agent | Runtime display name |
MULTICA_DAEMON_POLL_INTERVAL | 30s | Task polling interval when no wake event arrives |
MULTICA_DAEMON_HEARTBEAT_INTERVAL | 15s | Heartbeat interval |
MULTICA_DAEMON_MAX_CONCURRENT_TASKS | 20 | Concurrent task ceiling per daemon |
MULTICA_AGENT_TIMEOUT | 0 | Absolute time limit per run; 0 means no limit |
MULTICA_AGENT_IDLE_WATCHDOG | 30m | Silence ceiling with no output and no tool execution |
MULTICA_AGENT_TOOL_WATCHDOG | 2h | Silence ceiling for a single tool call |
MULTICA_OPENCODE_IDLE_WATCHDOG | 10m | OpenCode-specific silence threshold |
MULTICA_CODEX_SEMANTIC_INACTIVITY_TIMEOUT | 10m | Codex semantic-silence threshold |
MULTICA_CODEX_FIRST_TURN_TIMEOUT | 0 | Explicit override for the Codex first-turn no-progress ceiling; 0 keeps the default. The effective first-turn wait stays bounded by MULTICA_CODEX_SEMANTIC_INACTIVITY_TIMEOUT and the overall execution timeout — set MULTICA_CODEX_SEMANTIC_INACTIVITY_TIMEOUT strictly above this value (with some margin), or the wait is truncated to it and the model-catalog startup retry is skipped. Equal values are not enough: the semantic timer is armed first, so at equal durations the retry can still be lost |
MULTICA_CODEX_HANDSHAKE_TIMEOUT | 30s | Codex app-server startup handshake ceiling |
MULTICA_DAEMON_AUTO_UPDATE | Cloud true; self-hosted false | Whether to check for and apply CLI updates automatically |
MULTICA_DAEMON_AUTO_UPDATE_INTERVAL | 6h | Update check interval |
MULTICA_DAEMON_AUTO_RELOAD | true | Whether to restart into a multica binary replaced on disk out of band (brew upgrade, a re-download, a local build). Independent of MULTICA_DAEMON_AUTO_UPDATE |
MULTICA_WORKSPACES_ROOT | ~/multica_workspaces | Root directory for task working directories |
MULTICA_AGENT_TEMP_BASE | /tmp (Linux/macOS) | Linux/macOS only. Parent directory for private per-task temp dirs; must be an existing, writable absolute directory, and an invalid value fails task startup instead of falling back to /tmp. Pick a short path — child tools may bind AF_UNIX sockets under it, and sun_path is limited to 108 bytes on Linux and 104 on macOS |
MULTICA_KEEP_ENV_AFTER_TASK | false | Keep task directories for debugging |
Each AI coding tool accepts MULTICA_<PROVIDER>_PATH and MULTICA_<PROVIDER>_MODEL to override the command path and default model. QwenPaw is the exception: it has no MULTICA_QWENPAW_MODEL, because Multica never sends it a model — see AI coding tools comparison. DeepSeek Harness supports MULTICA_DSH_PATH and MULTICA_DSH_MODEL (a model id from the dsh catalog, e.g. deepseek-official/deepseek-chat). Machine-wide default arguments via MULTICA_<PROVIDER>_ARGS are currently supported for five tools: Claude Code, Codex, CodeBuddy, Qwen Code, and QwenPaw. The variables are MULTICA_CLAUDE_ARGS, MULTICA_CODEX_ARGS, MULTICA_CODEBUDDY_ARGS, MULTICA_QWEN_ARGS, and MULTICA_QWENPAW_ARGS. For example:
MULTICA_CLAUDE_PATH=/opt/bin/claude
MULTICA_CLAUDE_ARGS=--max-turns 40Precedence is command-line flag → environment variable → ~/.multica/config.json → built-in default. For watchdog behavior, see Daemon and runtimes.
Persisting daemon configuration
Common daemon-side settings can also be written to ~/.multica/config.json instead of relying on shell environment variables; named profiles keep theirs at ~/.multica/profiles/<name>/config.json:
multica config set poll_interval 10s
multica config showSupported keys:
| Key | Default | Description |
|---|---|---|
server_url | ws://localhost:8080/ws | Multica API / WebSocket URL |
app_url | empty | Web URL used for browser sign-in |
workspace_id | empty | Default workspace |
device_name | hostname | Device name shown in the runtime list |
runtime_name | Local Agent | Runtime display name |
workspaces_root | profile-aware path under ~ | Root directory for task working directories |
max_concurrent_tasks | 20 | Concurrent task ceiling; 0 or empty means unset |
poll_interval | 30s | Task polling interval |
heartbeat_interval | 15s | Heartbeat interval |
agent_timeout | unlimited | Absolute time limit per run |
codex_semantic_inactivity_timeout | 10m | Codex semantic-silence threshold |
codex_handshake_timeout | 30s | Codex app-server handshake ceiling |
disable_auto_update | follows environment | true turns auto-update off; false clears the local override and returns to the env var or default |
auto_update_check_interval | 6h | Update check interval |
disable_auto_reload | follows environment | true stops the daemon following a binary replaced on disk; false clears the local override. Resolved separately from disable_auto_update |
A few value rules:
- Duration keys accept positive Go durations (such as
10s,2h);0sand negative values are rejected. The one exception isagent_timeout:0sis valid and explicitly disables the run time limit. - Passing an empty string clears a persisted value and falls back to the env var or built-in default, for example
multica config set poll_interval "". max_concurrent_tasksrequires a non-negative integer.- Relative
workspaces_rootvalues are converted to absolute paths when saved.
Observability and analytics
| Variable | Default | Description |
|---|---|---|
ANALYTICS_DISABLED | false | Set true to turn off PostHog reporting |
POSTHOG_API_KEY | empty | Reporting is off when unset; set it to use your own PostHog project |
POSTHOG_HOST | https://us.i.posthog.com | PostHog host |
METRICS_ADDR | empty | Prometheus metrics listen address; empty means not started |
REALTIME_METRICS_TOKEN | empty | Bearer token protecting /health/realtime |
Next steps
- Sign-in and signup — sign-in methods and signup restrictions.
- Troubleshooting — symptom-based diagnosis of deployment problems.
- Daemon and runtimes — the daemon-side counterparts of this configuration.