Multica / Self-deployment and reference
environment variables
List of environment variables that need to be configured for the self-host Multica server.

Overview
Multica's self-deployment reads configuration from environment variables when the server starts - database, login, email, storage, and registration whitelist are all configured here. This page is grouped into complete lists by purpose: each group explains what will happen if it is not installed, and which ones are necessary for production. How do those related to Auth really match the login and registration configuration.
Core server environment variables
These are the core variables you must consider before deploying - some have default values to enable the server to start, but in a production environment you should explicitly configure the required ones.
| Environment variables | Default value | Must be set for production? |
|---|---|---|
DATABASE_URL |
postgres://multica@localhost:5432/multica?sslmode=disable |
YES |
PORT |
8080 |
No (unless changing port) |
JWT_SECRET |
multica-dev-secret-change-in-production |
Yes (default is unsafe) |
APP_ENV |
empty | yes (must be production) |
FRONTEND_ORIGIN |
Empty | Yes (self-host should fill in your own domain name) |
MULTICA_DEV_VERIFICATION_CODE |
Empty | No (must remain empty for production) |
**Production environments keep
MULTICA_DEV_VERIFICATION_CODEempty. ** Fixed local test verification codes being turned off by default; if you setMULTICA_DEV_VERIFICATION_CODE=888888, whenAPP_ENVis not in production, anyone who can request a verification code can log in with this fixed value. This shortcut code will be ignored whenAPP_ENV=production.
Database connection pool
| Environment variables | Default value | Description |
|---|---|---|
DATABASE_MAX_CONNS |
25 |
The maximum number of connections for pgxpool. Daemon polling frequently (every 3 seconds) consumes connections; large-scale deployments may need to be increased |
DATABASE_MIN_CONNS |
5 |
Minimum resident connection |
When not set uses the default value in the above table, not pgx's built-in 4/NumCPU - the latter has caused the connection pool to be exhausted in production.
How to configure email
Multica supports two email sending channels - Resend is suitable for public network deployment, and SMTP relay is suitable for intranet/self-deployment. When set simultaneously, SMTPHOST has higher priority than RESENDAPIKEY.
Resend
| Environment variables | Default value | Description |
|---|---|---|
RESEND_API_KEY |
Empty | Resend API key |
RESEND_FROM_EMAIL |
noreply@multica.ai |
Sending address (must be a domain name that has been verified by the Resend account; it also serves as the From: header when using SMTP) |
SMTP relay
| Environment variables | Default value | Description |
|---|---|---|
SMTP_HOST |
Null | SMTP relay host name. Once set, SMTP mode is enabled and overrides Resend |
SMTP_PORT |
25 |
SMTP port. Use 587 for STARTTLS submission port; use 465 for SMTPS (implicit TLS, automatically enabled) |
SMTP_USERNAME |
Empty | SMTP username. Leave blank to indicate unauthenticated relay |
SMTP_PASSWORD |
Empty | SMTP password |
SMTP_TLS |
starttls |
TLS mode. implicit (alias smtps, ssl) performs an immediate TLS handshake (SMTPS) on connection; port 465 is automatically enabled. If not set / starttls will be upgraded via STARTTLS after connecting |
SMTP_TLS_INSECURE |
false |
Set to true to skip TLS certificate verification (private CA/self-signed certificates only) |
SMTP_EHLO_NAME |
Machine hostname | EHLO/HELO name advertised to relay. When a strict relay (such as Google Workspace smtp-relay.gmail.com) refuses the default greeting from a public IP, fill in a real FQDN - otherwise the relay will directly disconnect and behave as an incomprehensible EOF on a subsequent command |
The server will automatically upgrade when advertising STARTTLS. The dial timeout is 10s, and the entire SMTP session has a 30s deadline to prevent the relay black hole from hanging up the auth handler.
Neither behavior: The server will not report an error, but all emails (verification codes, invitation links) that should be sent only hit the stdout of the server. Local development is convenient (you copy the verification code from the server log); If you forget to set up the production environment as a black hole, users will not receive emails and there will be no error prompts.
How to configure Google OAuth
Optional. If not set, only email + verification code will be used to log in; if set, "Log in with Google" will appear on the login page.
| Environment variables | Default value | Description |
|---|---|---|
GOOGLE_CLIENT_ID |
Empty | Google Cloud OAuth client ID |
GOOGLE_CLIENT_SECRET |
Empty | Google Cloud OAuth secret |
GOOGLE_REDIRECT_URI |
http://localhost:3000/auth/callback |
OAuth callback address (replace self-host with your front-end domain name) |
Hot-validation: The front-end gets these configurations through /api/config when it is running. There is no need to restart the front-end or rebuild the image after the change - just restart the server after the change.
For complete configuration steps (including Google Cloud Console operations), see [Login and Registration Configuration](/vibe-coding-tools/multica/auth-setup#How to configure-google-oauth).
How to configure file storage
Multica stores attachments uploaded by users (pictures, files, etc. in comments). Give priority to S3; if S3 is not available, fall back to the local disk.
S3 / S3 compatible storage
| Environment variables | Default value | Description |
|---|---|---|
S3_BUCKET |
Empty | Only fill in the bucket name (for example my-bucket), Do not bring the .s3.<region>.amazonaws.com suffix - the server will use S3_BUCKET + S3_REGION to create the public host by itself. Enable S3 storage once set |
S3_REGION |
us-west-2 |
AWS Region. Must be consistent with the region where the bucket is located - it is used for both SDK signature and public URL |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
Empty | Static credentials. Use the AWS SDK default credential chain (IAM role/environment credentials) when not configured at all |
AWS_ENDPOINT_URL |
Empty | Custom S3 compatible endpoint (e.g. MinIO). If set, it will switch to path-style URL |
ATTACHMENT_DOWNLOAD_MODE |
auto |
Attachment download path: auto, cloudfront, presign or proxy. When CloudFront is fully configured under auto, CloudFront will be given priority; for intranet/private endpoint hosts, server proxy will be used; for public network S3-compatible endpoints, presigned GET will be used when supported. |
ATTACHMENT_DOWNLOAD_URL_TTL |
30m |
The validity period for CloudFront signed URLs and S3 presigned download URLs. Using Go duration format |
S3_BUCKET not set: The info log "S3_BUCKET not set, cloud upload disabled" will be printed when the server starts, and all uploads will fall back to the local disk.
Object Storage URL Assemble by priority:
- Set up
CLOUDFRONT_DOMAIN→https://CLOUDFRONT_DOMAIN/<key> - Set
AWS_ENDPOINT_URL→AWS_ENDPOINT_URL/S3_BUCKET/<key>` (path-style) - AWS S3 →
https://S3_BUCKET.s3.S3_REGION.amazonaws.com/<key>(virtual-hosted-style) by default. When the bucket name contains a dot, it will fall back tohttps://s3.S3_REGION.amazonaws.com/S3_BUCKET/<key>(path-style), because the AWS wildcard certificate cannot cover the host containing the dot.
The download_url returned by the API points to GET /api/attachments/{id}/download when CloudFront signing is not configured. This endpoint will jump to the CloudFront/S3 presigned URL when it is safe; when it encounters a private or intranet endpoint such as http://rustfs:9000, it will be streamed by the server. Docker/VPC internal object storage recommends explicitly setting ATTACHMENT_DOWNLOAD_MODE=proxy.
Local disk (when S3 is not configured)
| Environment variables | Default value | Description |
|---|---|---|
LOCAL_UPLOAD_DIR |
./data/uploads |
Local storage directory |
LOCAL_UPLOAD_BASE_URL |
Empty (returns relative path) | Publicly accessible base URL - the full URL of the attachment cannot be obtained without a front end |
CloudFront (optional)
If you use CloudFront as a CDN for S3, three relevant environment variables: CLOUDFRONT_DOMAIN, CLOUDFRONT_KEY_PAIR_ID, CLOUDFRONT_PRIVATE_KEY (or CLOUDFRONT_PRIVATE_KEY_SECRET read from Secrets Manager). Don't worry if you don't use CloudFront - it doesn't conflict with configuring S3.
Cookie domain
| Environment variables | Default value | Description |
|---|---|---|
COOKIE_DOMAIN |
NULL | The scope of the session cookie |
- Empty: The cookie is only effective for the host visited (single host deployment is correct)
- Set to
.example.com: the cookie is shared across all subdomains (letapp.example.comandapi.example.comshare login status) - ⚠️ Cannot be an IP address (will be ignored by the browser)
How to restrict who can register
Three-tier whitelists are combined according to priority. Once the whitelist at any level is set to be non-empty, unmatched mailboxes will be rejected - even ALLOWSIGNUP=true cannot be blocked.
| Environment variables | Default value | Description |
|---|---|---|
ALLOWED_EMAILS |
Empty | Explicit email whitelist (comma separated). If it is not empty, only the emails in the list can be registered |
ALLOWED_EMAIL_DOMAINS |
Empty | Domain name whitelist (comma separated). If it is not empty, only the domain names in the list can be registered |
ALLOW_SIGNUP |
true |
Register the master switch. Set false to completely turn off registration |
Unintuitive point: The combination of ALLOWED_EMAIL_DOMAINS=company.io + ALLOW_SIGNUP=true is not **"allow company.io or everyone", but only allows company.io. The AND semantics of the whitelist - the decision tree is detailed in [Login and Registration Configuration → Signup Whitelist] (/auth-setup#How to restrict who can register).
The invitation process itself does not check the signup whitelist - but the invitee must first be able to log in before they can accept the invitation. If the other party already has a Multica account (for example, registered in other workspaces), they can accept it directly without being affected by the whitelist; If the other party has not registered yet, the first step in their login (sending a verification code) will still pass the whitelist check. Emails rejected by ALLOW_SIGNUP=false or ALLOWED_EMAILS / ALLOWED_EMAIL_DOMAINS cannot complete the registration and cannot accept the invitation**.
Lock workspace creation
ALLOWSIGNUP=false can block new registrations, but it cannot prevent already logged-in users from continuing to POST /api/workspaces to open new workspaces by themselves. In self-deployed instances where you want the platform administrator to be able to see all issues/repositories/agents, turn on DISABLEWORKSPACECREATION=true to close this gap.
| Environment variables | Default value | Description |
|---|---|---|
DISABLE_WORKSPACE_CREATION |
false |
When set to true, any call to POST /api/workspaces returns 403 workspace creation is disabled for this instance. The web UI hides all workspace creation entry points through /api/config. This switch is instance-level; there is no owner or admin exception |
Recommended server opening process:
- Start the instance with
DISABLE_WORKSPACE_CREATIONunset, which is the default. - The administrator logs in and creates a shared workspace.
- Set
DISABLE_WORKSPACE_CREATION=trueand restart the backend. New users will then be able to join only by invitation.
If you still want the invited new users to complete the first registration, keep ALLOW_SIGNUP=true (if necessary, use ALLOWED_EMAIL_DOMAINS / ALLOWED_EMAILS to limit the registerable email addresses), and just turn on DISABLE_WORKSPACE_CREATION=true; if you set ALLOW_SIGNUP=false at the same time, even the email addresses with pending invites cannot complete the first registration, and it is only suitable for all members who already have it. Example of Multica account.
Rate limiting (optional Redis)
Public authentication endpoints, including /auth/send-code, /auth/verify-code, and /auth/google, are protected by fixed-window per-IP rate limits. The current limiter backend is Redis. When REDIS_URL is not set, the middleware passes through fail-open, and the backend logs `rate limiting disabled: REDIS_URL not configured` at startup.
| Environment variables | Default value | Description |
|---|---|---|
REDIS_URL |
Empty | Redis connection URL (e.g. redis://localhost:6379/0). Without setting the current limit function of the authentication endpoint, it is directly turned off. The same Redis is also reused by real-time event fan-out, PAT cache, and daemon token cache; when not configured, these components fall back to memory mode/direct check DB |
RATE_LIMIT_AUTH |
5 |
Maximum number of requests per minute to /auth/send-code and /auth/google from a single IP |
RATE_LIMIT_AUTH_VERIFY |
20 |
Maximum number of requests to /auth/verify-code per minute from a single IP |
RATE_LIMIT_TRUSTED_PROXIES |
Empty | A comma-separated list of CIDRs from which source IPs are allowed to identify clients via X-Forwarded-For. Default empty = Never trust XFF, the current limiter only looks at directly connected RemoteAddr |
Throttled requests will return 429 Too Many Requests, with a Retry-After: 60 header and a {"error":"too many requests"} response body.
**
RATE_LIMIT_TRUSTED_PROXIESmust be set when deployed behind a reverse proxy. ** Otherwise, from the backend perspective, all real users share the proxy IP, and the entire deployment falls into the same bucket, and/auth/send-codewill become that the entire site can only send 5 times per minute. Common values: local Caddy / Nginx uses127.0.0.1/32,::1/128; Cloudflare / ALB / CloudFront uses the CDN IP segment disclosed by each company. Only requests whoseRemoteAddrfalls within these CIDRs are allowed to rewrite the client IP viaX-Forwarded-For.
Here RATE_LIMIT_TRUSTED_PROXIES and MULTICA_TRUSTED_PROXIES are not the same variable - the latter controls the rate limiter of the autopilot webhook endpoint (/api/webhooks/autopilots/{token}). The two current limiters each read their own lists, and the instance deployed behind the proxy needs to be configured with both.
Tuning parameters for the daemon process
The daemon runs on the user's local machine, and the configuration also reads local environment variables. Some commonly used ones:
| Environment variables | Default value | Description |
|---|---|---|
MULTICA_SERVER_URL |
ws://localhost:8080/ws |
server address (replace self-host with your domain name) |
MULTICA_DAEMON_HEARTBEAT_INTERVAL |
15s |
Heartbeat rate |
MULTICA_DAEMON_POLL_INTERVAL |
3s |
Task polling frequency |
MULTICA_DAEMON_MAX_CONCURRENT_TASKS |
20 |
Maximum number of concurrent tasks |
MULTICA_AGENT_TIMEOUT |
0 |
The absolute wall clock upper limit for a single task; 0 = no upper limit, the task is only subject to watchdog constraints (active tasks will not be killed because they run too long). Set another positive value when you want a hard cost/resource ceiling |
MULTICA_AGENT_IDLE_WATCHDOG |
30m |
Idle watchdog: backend continues to be silent (no messages, message queue is empty, and no tools are in transit) for so long before force-stop. 0 = disable the entire set of watchdogs |
MULTICA_AGENT_TOOL_WATCHDOG |
2h |
The upper limit of silence when the tool is in transit: If there is no output for a long time after a tool call is issued (suspected of a stuck child process), it will force-stop. 0 = close the pocket (tools in transit are never stopped) |
MULTICA_PROVIDER_PATH |
Corresponding CLI name | The executable file path of each AI programming tool (such as MULTICA_CLAUDE_PATH) |
MULTICA_PROVIDER_MODEL |
Empty | The default model of each AI programming tool |
MULTICA_PROVIDER_ARGS |
NULL | Daemon-level default CLI arguments that apply to every task in this backend, preceded by each agent's own custom_args. Supports MULTICA_CLAUDE_ARGS, MULTICA_CODEX_ARGS, MULTICA_CODEBUDDY_ARGS |
For a complete explanation of the effect of each parameter on daemon behavior, see Daemons and Runtimes.
Default agent parameters (MULTICA_PROVIDER_ARGS)
Setting up a fleet-wide default CLI argument for a backend - makes it easy to apply a default cost or resource baseline (e.g. --max-turns) to all agents on a daemon without having to modify each agent's custom_args individually. This is a layer of defaults, rather than an unbreakable hard cap: each agent's own custom_args is appended, and can be overridden (see priority below).
- Priority: The default parameters take effect first, and then each agent's own
custom_argsis appended. For parameters with values, the downstream CLI's own parameter parser determines the final effective value (most tools use "the latter override"), so a single agent can increase the default value of a daemon process, but where the agent does not override it, the default value still takes effect. - Parsing: The value is split according to POSIX shell-word rules, so quotes are available -
MULTICA_CLAUDE_ARGS='--append-system-prompt "multi word"'will be parsed into two tokens. - Security: The default parameter layer and each agent's
custom_argslayer are filtered by the same set of blocked-flags, so protocol key flags (such as Claude's-p,--output-format,--input-format,--permission-mode,--mcp-config, and Codex's--listen) cannot be injected from either layer. - Unset/Empty means no change in behavior.
Front-end access control
| Environment variables | Default value | Description |
|---|---|---|
FRONTEND_ORIGIN |
Empty | The front-end address. The links in the invitation email, CORS whitelist, and cookie domain are all derived from here. If the email link is not set, it will fallback to the hosted domain name https://app.multica.ai——self-host must be filled in explicitly |
MULTICA_APP_URL |
NULL | The front-end URL used by the CLI login process. The Web UI will also use it to display the self-host daemon setup command with your own app domain; in a same-origin deployment, if MULTICA_PUBLIC_URL is not set, it will also serve as the daemon's server_url |
MULTICA_PUBLIC_URL |
Empty | Public API URL without trailing slash. server_url used for autopilot webhook URL, also used as daemon by Web UI |
CORS_ALLOWED_ORIGINS |
NULL | Additional CORS allowed origins (comma separated) |
ALLOWED_ORIGINS |
Empty | WebSocket-specific origin whitelist (comma separated); if not set, fall back in the order of CORS_ALLOWED_ORIGINS → FRONTEND_ORIGIN → localhost:3000/5173/5174 |
FRONTEND_ORIGINwill result in two silent failures: (1) The link in the invitation email points tohttps://app.multica.ai(the domain name of the hosted version), and the user cannot click back to your self-host instance; (2) The Origin verification of the WebSocket connection falls back tolocalhost:3000 / 5173 / 5174, the WebSocket deployed in production All were rejected, and the front end looked like "real-time updates are not working."
GitHub integration
GitHub PR ↔ issue integration relies on two required environment variables. Only when both are matched will enable Connect GitHub in Settings and accept webhooks. The other two optional variables are used to directly get the associated account name during installation.
| Environment variables | Default value | Description |
|---|---|---|
GITHUB_APP_SLUG |
Empty | Your GitHub App slug (the tail of https://github.com/apps/<slug>). Settings → Use it to spell the jump URL of the install button in GitHub |
GITHUB_WEBHOOK_SECRET |
Empty | The webhook secret you set on GitHub App. Each pull_request / installation delivery uses it for HMAC-SHA256 verification; the same value is also used as the signing key for the state token in the setup callback |
GITHUB_APP_ID |
Empty | Optional. The numeric App ID on the app settings page. Use it with GITHUB_APP_PRIVATE_KEY to let the setup callback get the associated account name directly from GitHub at the moment of installation |
GITHUB_APP_PRIVATE_KEY |
Empty | Optional. The complete PEM block of the App RSA private key (containing the two lines -----BEGIN/END-----, retaining newlines). Short-lived JWT required to sign GitHub App authentication REST calls |
When any required variable is not set:
- Settings → GitHub
Connect GitHubbutton disable, display "not configured" prompt to admin /api/webhooks/githubdirectly returns503 github webhooks not configured- when the secret is not configured, Multica refuses to process any webhook events, instead of treating all signatures as valid
**Optional GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY When not set: **
- After the installation is complete, the connection card will briefly display
Connected to unknown. When GitHub'sinstallation.createdwebhook arrives (usually within a few seconds), Multica will refresh the row to the real organization/user name, and use realtime push to update the currently open Settings → GitHub page without manual refresh.
Note: GITHUB_WEBHOOK_SECRET is also reused as the signature key of the state token in the install process, so operation and maintenance only needs to maintain one secret. It is not the Client secret of GitHub App - the Client secret is used by OAuth and has nothing to do with this integration. For the complete configuration process, see GitHub Integration → Self-Host Configuration.
Usage statistics
By default, it is reported to Multica’s official PostHog instance. If you don't want to report it, just set ANALYTICSDISABLED=true.
| Environment variables | Default value | Description |
|---|---|---|
ANALYTICS_DISABLED |
false |
Set true to completely turn off backend reporting |
POSTHOG_API_KEY |
Built-in default key | Fill in when changing to your own PostHog instance |
POSTHOG_HOST |
https://us.i.posthog.com |
If you build your own PostHog, change it to your own address |
Next step
- Login and Registration Configuration - How to configure the environment variables related to auth above and where are the traps?
- GitHub Integration —— How to build the GitHub App behind
GITHUB_APP_SLUG/GITHUB_WEBHOOK_SECRET - Troubleshooting - Common mismatch symptoms and fixes
- Daemon Processes and Runtimes - Behavioral meaning of
MULTICA_DAEMON_*parameters