mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
router.go reads MULTICA_WECOM_DEFAULT_LOCALE to pick the language the bot writes its own copy in for readers it cannot look a language up for (a group chat, anyone not linked to a Multica account). Neither .env.example nor docker-compose.selfhost.yml named it, so a self-hosted operator had no way to find the knob, and setting it on the host did nothing: compose forwards only what its environment: block lists, and the deployment silently stayed on zh-Hans. Add it to both, and a test that parses router.go for every MULTICA_WECOM_* literal it reads and fails when either file is missing one.
753 lines
42 KiB
Bash
753 lines
42 KiB
Bash
# Database
|
|
POSTGRES_DB=multica
|
|
POSTGRES_USER=multica
|
|
POSTGRES_PASSWORD=multica
|
|
POSTGRES_PORT=5432
|
|
DATABASE_URL=postgres://multica:multica@localhost:5432/multica?sslmode=disable
|
|
# Optional pgxpool tuning. Defaults are 25 / 5 per pod and are usually fine.
|
|
# You can also set pool_max_conns / pool_min_conns as query params on
|
|
# DATABASE_URL; env vars below take precedence over URL params.
|
|
# DATABASE_MAX_CONNS=25
|
|
# DATABASE_MIN_CONNS=5
|
|
# Search transactions default to a 64 MB PostgreSQL work_mem ceiling per plan
|
|
# node. Set 1-64 to lower it for memory-constrained databases, or 0 to keep the
|
|
# database/session default. Values above 64 or invalid values fall back to 64.
|
|
# DATABASE_SEARCH_WORK_MEM_MB=64
|
|
# Optional PostgreSQL read replica. The API keeps all existing reads on primary
|
|
# until a business explicitly opts into eventual-consistency routing in code.
|
|
# Every new replica connection is required to be read-only and is recycled
|
|
# after 5m by default so a promoted node is revalidated promptly. Replica
|
|
# failures fall back to primary and open a short passive circuit; no background
|
|
# database probe is added. Replica reads may return stale data without a bound.
|
|
# DATABASE_REPLICA_URL=postgres://multica:multica@replica:5432/multica?sslmode=require&target_session_attrs=read-only
|
|
# Replica pool sizing is independent so enabling it does not clone the primary
|
|
# pool's 25 / 5 connection budget per API process.
|
|
# DATABASE_REPLICA_MAX_CONNS=10
|
|
# DATABASE_REPLICA_MIN_CONNS=0
|
|
# Retry transient database failures for up to this duration across the image
|
|
# entrypoint's migration and API startup phases. Set to 0 for a single
|
|
# fail-fast attempt per phase. Each connection attempt is separately bounded
|
|
# so an unreachable host cannot consume the whole budget. A native pgx
|
|
# connect_timeout in DATABASE_URL, PGCONNECT_TIMEOUT, or a service file takes
|
|
# precedence over MULTICA_DATABASE_CONNECT_TIMEOUT for compatibility.
|
|
# MULTICA_DATABASE_STARTUP_TIMEOUT=3m
|
|
# MULTICA_DATABASE_CONNECT_TIMEOUT=5s
|
|
|
|
# Server
|
|
# APP_ENV gates production safety checks. Docker self-host pins APP_ENV to
|
|
# "production" by default. Local dev can leave it unset.
|
|
# See SELF_HOSTING.md for the full login setup.
|
|
APP_ENV=
|
|
# Optional local/testing shortcut. Empty by default, so there is no fixed
|
|
# verification code. Without RESEND_API_KEY, generated codes print to stdout.
|
|
# If you need deterministic local automation, set a 6-digit value such as
|
|
# 888888 and keep APP_ENV non-production. This is ignored when APP_ENV=production.
|
|
MULTICA_DEV_VERIFICATION_CODE=
|
|
# Backend port. This is the one to edit: it is both the port the backend process
|
|
# listens on for a local/bare run, and the host port the Docker Compose
|
|
# self-host stack publishes. In Compose the container always listens on 8080
|
|
# internally, so changing this never needs a rebuild.
|
|
PORT=8080
|
|
# Optional aliases that override PORT for the backend, in this order. Leave them
|
|
# commented unless you specifically need the host port to differ from the port
|
|
# the process listens on. An uncommented value here wins over PORT everywhere
|
|
# (Makefile, scripts/local-env.sh, docker-compose.selfhost.yml), and because
|
|
# this file is read by make it also overrides the same variable coming from your
|
|
# shell environment.
|
|
# BACKEND_PORT=8080
|
|
# API_PORT=8080
|
|
# SERVER_PORT=8080
|
|
FRONTEND_PORT=3000
|
|
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_PORT.
|
|
# Set explicitly only when serving frontend on a different origin/domain.
|
|
FRONTEND_ORIGIN=http://localhost:${FRONTEND_PORT}
|
|
# Prometheus metrics are disabled by default. When enabled, bind to loopback
|
|
# unless you protect the listener with private networking, allowlists, or
|
|
# proxy auth. Do not expose this endpoint through the public app/API ingress.
|
|
# HTTP request metrics start accumulating only when this listener is enabled.
|
|
# METRICS_ADDR=127.0.0.1:9090
|
|
# Optional container-loopback maintenance API. Use /app/maintenance via container exec.
|
|
# Never publish this port. See docs/maintenance-jobs.md. Empty disables it.
|
|
MAINTENANCE_PORT=
|
|
# REQUIRED. Signing key for auth JWTs, daemon JWTs, and avatar/attachment
|
|
# URL capability HMACs. Generate a strong random value:
|
|
# macOS/Linux: openssl rand -hex 32
|
|
# Windows (PowerShell): $b = New-Object byte[] 32; $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create(); $rng.GetBytes($b); $rng.Dispose(); -join ($b | ForEach-Object { $_.ToString('x2') })
|
|
# Leave empty ONLY for local non-Docker development (APP_ENV unset) — the
|
|
# server then uses a dev-only default and logs a warning. docker-compose
|
|
# refuses to start without a value, and production refuses to boot on the
|
|
# dev default or any known placeholder.
|
|
JWT_SECRET=
|
|
# Derived by Makefile / local scripts from the backend port.
|
|
# Set explicitly only when the daemon reaches the API through a different URL.
|
|
# MULTICA_SERVER_URL=ws://localhost:8080/ws
|
|
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_ORIGIN.
|
|
# Set explicitly only when the app's public URL differs from local frontend.
|
|
MULTICA_APP_URL=${FRONTEND_ORIGIN}
|
|
# Public URL the API is reachable at from the open internet (no trailing
|
|
# slash). Used to mint absolute webhook URLs for autopilot webhook
|
|
# triggers and to show correct daemon setup commands in the web UI. Leave
|
|
# unset behind a same-origin reverse proxy or for plain localhost dev —
|
|
# the frontend will compose the URL from window.origin + webhook_path in
|
|
# that case. Headers are intentionally not used to derive this value, to
|
|
# avoid Host / X-Forwarded-Host spoofing when a self-hosted reverse proxy
|
|
# is not hardened.
|
|
MULTICA_PUBLIC_URL=
|
|
# Optional API URL used only in self-host daemon setup commands. Set this
|
|
# when the daemon reaches the API through a different URL than the public
|
|
# webhook URL above. Falls back to MULTICA_PUBLIC_URL, then MULTICA_APP_URL.
|
|
MULTICA_DAEMON_SERVER_URL=
|
|
# Optional delay after SIGTERM/SIGINT before the existing graceful shutdown
|
|
# begins. Accepts a non-negative Go duration such as 300s or 5m. Empty or 0
|
|
# preserves the default behavior and starts shutdown immediately. The platform
|
|
# termination grace period must cover this hold plus the later shutdown work.
|
|
MULTICA_SHUTDOWN_HOLD_DURATION=
|
|
# How long an offline runtime may reconnect before its in-flight tasks fail.
|
|
# Accepts a positive Go duration. The default is 3h; values below 150s are
|
|
# clamped to the runtime heartbeat freshness window.
|
|
MULTICA_RUNTIME_RECONNECT_GRACE=3h
|
|
# Comma-separated CIDR list of reverse proxies whose X-Forwarded-For /
|
|
# X-Real-IP headers the per-IP webhook rate limiter is allowed to trust.
|
|
# Empty (the default) means "trust no headers" — the limiter uses
|
|
# r.RemoteAddr only, which is the safe shape when the backend is
|
|
# exposed directly. Set this when running behind nginx/Caddy/Cloudflare:
|
|
# e.g. "127.0.0.1/32" for a same-host reverse proxy, or the CDN's
|
|
# announced ranges for cloud deployments.
|
|
MULTICA_TRUSTED_PROXIES=
|
|
# Server-side assist layer (MUL-4238). These variables govern the LLM calls the
|
|
# API process makes on its own — every one of them goes through server/pkg/llm,
|
|
# and that package stays inert until an API key or a base URL is set here.
|
|
# Access is internal-only: the generic OpenAI-compatible passthrough endpoints
|
|
# were removed in MUL-4309.
|
|
#
|
|
# This is NOT the deployment's only path from chat content to a model. Running
|
|
# an agent is a separate one: the daemon executes an AI coding tool with that
|
|
# tool's own credentials, and does not forward these MULTICA_LLM_* settings to
|
|
# it. (The task-scoped Multica connection variables the agent needs — server
|
|
# URL, task token — are injected by the daemon separately.) Nothing below
|
|
# governs that path.
|
|
#
|
|
# Once configured, the layer has exactly two consumers, and this is what each
|
|
# one sends upstream:
|
|
# - Chat auto-titling: the first user message of a new chat session, verbatim
|
|
# and uncapped. Attachments are never included.
|
|
# - Chat follow-up questions (the suggestion buttons under an agent reply):
|
|
# the tail of the conversation — up to 6 messages, with the reply being
|
|
# answered capped at 3000 characters (2000 head + 1000 tail) and each
|
|
# older message at 800.
|
|
#
|
|
# Leaving BOTH the API key and the base URL empty is a fully supported
|
|
# configuration, and the right one when policy forbids this layer sending chat
|
|
# content anywhere: it is disabled, makes zero upstream requests, and neither
|
|
# feature above sends anything. Nothing breaks — chats keep the title the
|
|
# client derives from the first message (30 characters, no model involved) and
|
|
# the follow-up question buttons simply never appear. server/pkg/llm asserts
|
|
# both the zero-request behaviour and the two-consumer list above, so neither
|
|
# can drift from this comment silently.
|
|
# - API key for the upstream (OpenAI or any OpenAI-compatible gateway).
|
|
MULTICA_LLM_API_KEY=
|
|
# - Base URL of the upstream. Leave unset to use OpenAI's default
|
|
# (https://api.openai.com/v1). Set this to point at a gateway or a
|
|
# self-hosted, OpenAI-compatible model server.
|
|
MULTICA_LLM_BASE_URL=
|
|
# - Default model used when a request omits `model`. When unset, falls
|
|
# back to a small built-in default (gpt-6-luna).
|
|
MULTICA_LLM_DEFAULT_MODEL=
|
|
# - Transport retry budget for every upstream LLM request. This is the one
|
|
# canonical source; the server logs the effective policy at startup
|
|
# ("llm retry policy").
|
|
# unset use the built-in default of 2 retries
|
|
# 0 disable retries — exactly one request per call
|
|
# 1..5 at most N retries, so at most N+1 requests per call
|
|
# Anything else (negative, non-numeric, above 5) fails the boot rather than
|
|
# being corrected silently. The ceiling is a latency budget: backoff starts
|
|
# at 0.5s and doubles to an 8s cap, so 6 retries would sleep ~21s while the
|
|
# internal callers of this layer time out after 8s and 20s.
|
|
# Retried: connection failures, HTTP 408/409/429 and 5xx. Not retried: every
|
|
# other 4xx. This budget does not cover a stream that breaks mid-response,
|
|
# and it is unrelated to task-level retries.
|
|
MULTICA_LLM_MAX_RETRIES=
|
|
# - Ask an OpenAI-compatible gateway to turn the model's reasoning
|
|
# ("thinking") off by adding chat_template_kwargs: {"enable_thinking":
|
|
# false} to every request body this layer sends. Some gateways and
|
|
# GLM/Qwen-style model routes honor the field; there it removes the
|
|
# thinking pass that would otherwise dominate the latency budget of the
|
|
# assist calls above. Standard OpenAI endpoints reject unknown body
|
|
# fields, so only enable this when the upstream accepts it. GPT-5.6-family
|
|
# models and GPT-6 Luna get reasoning_effort=none on the follow-up questions
|
|
# request. Chat auto-titling sends no reasoning field; this switch affects
|
|
# it only when the configured upstream accepts chat_template_kwargs.
|
|
# Accepted values are true/false and 1/0
|
|
# (case-insensitive); anything else fails the boot. The startup log
|
|
# ("llm retry policy") reports the effective state.
|
|
MULTICA_LLM_DISABLE_THINKING=
|
|
MULTICA_DAEMON_CONFIG=
|
|
MULTICA_WORKSPACE_ID=
|
|
MULTICA_DAEMON_ID=
|
|
MULTICA_DAEMON_DEVICE_NAME=
|
|
MULTICA_DAEMON_POLL_INTERVAL=30s
|
|
# Independent healthy-WebSocket safety poll upper bound; does not replace the ordinary poll interval.
|
|
MULTICA_DAEMON_WS_CLAIM_POLL_INTERVAL=3m
|
|
MULTICA_DAEMON_HEARTBEAT_INTERVAL=15s
|
|
MULTICA_CODEX_PATH=codex
|
|
MULTICA_CODEX_MODEL=
|
|
MULTICA_CODEX_WORKDIR=
|
|
|
|
# Feature flags
|
|
# Optional path to a YAML file declaring feature flag rules. When unset,
|
|
# every flag falls through to the caller's default, which lets the server
|
|
# boot before any flag config is authored. When set, the file is read once
|
|
# at startup and a parse / IO error fails fast — same loud-failure shape as
|
|
# DATABASE_URL or JWT_SECRET misconfig. See server/pkg/featureflag for the
|
|
# full schema; the minimum example is:
|
|
#
|
|
# billing_new_invoice_email:
|
|
# default: true
|
|
# checkout_algo:
|
|
# default: false
|
|
# variant: experiment-v2
|
|
# percent: { percent: 25, by: user_id }
|
|
#
|
|
# Individual flags can also be overridden without touching the YAML by
|
|
# setting FF_<FLAG_KEY> env vars (FF_BILLING_NEW_INVOICE_EMAIL=false, 25%,
|
|
# or any variant string). The env override beats the YAML, which is the
|
|
# Ops kill-switch path — flip a flag without redeploying by restarting the
|
|
# process with the env var set.
|
|
MULTICA_FEATURE_FLAGS_FILE=
|
|
|
|
# Web/Desktop keep a local search index per workspace and sync it through
|
|
# /api/search-index. On by default; turning it off makes clients stop syncing
|
|
# and search through the server again:
|
|
# FF_LOCAL_SEARCH_INDEX=off
|
|
|
|
# Workspace subscription Checkout/Portal/entitlement proxying is fail-closed;
|
|
# enable it only after multica-cloud billing.subscriptions is configured:
|
|
# FF_BILLING_WORKSPACE_SUBSCRIPTIONS=true
|
|
#
|
|
# Point multica-cloud's three subscription return URLs at this app's
|
|
# /billing/return page (checkout_success_url, checkout_cancel_url,
|
|
# portal_return_url). Cloud appends the workspace it authorized plus a result
|
|
# marker, and that page resolves them to the workspace's Billing settings.
|
|
# Configuring a workspace-specific URL instead would strand every other
|
|
# workspace on someone else's page.
|
|
|
|
# Managed multica-cloud connection. Leave the URL empty for self-hosting. When
|
|
# connected, Fleet, Billing, entitlement policy, and strict pre-purchased seat
|
|
# capacity all use this service. Authentication and network isolation for these
|
|
# internal routes are owned by the deployment platform.
|
|
MULTICA_CLOUD_URL=
|
|
|
|
# Self-host image channel
|
|
# Default stable release channel. Pin to an exact release like v0.2.4 if you
|
|
# want to stay on a specific version. If the selected tag has not been
|
|
# published to GHCR yet, use make selfhost-build / the build override instead.
|
|
MULTICA_IMAGE_TAG=latest
|
|
MULTICA_BACKEND_IMAGE=ghcr.io/multica-ai/multica-backend
|
|
MULTICA_WEB_IMAGE=ghcr.io/multica-ai/multica-web
|
|
|
|
# Email
|
|
# Two delivery options - only one needs to be configured:
|
|
#
|
|
# Option A: Resend (SaaS, recommended for cloud deployments)
|
|
# Set RESEND_API_KEY to a key from resend.com and verify your sending domain there.
|
|
# For local/dev use, leave RESEND_API_KEY empty - codes print to stdout. To
|
|
# accept a fixed local code, also set MULTICA_DEV_VERIFICATION_CODE above
|
|
# (ignored when APP_ENV=production).
|
|
RESEND_API_KEY=
|
|
RESEND_FROM_EMAIL=[email protected]
|
|
#
|
|
# Option B: SMTP relay (for self-hosted / on-premise deployments)
|
|
# Takes priority over Resend when SMTP_HOST is set.
|
|
# Supports unauthenticated relay (leave SMTP_USERNAME empty) and authenticated SMTP.
|
|
# SMTP_FROM_EMAIL is the envelope/header sender for SMTP. If unset, SMTP
|
|
# falls back to RESEND_FROM_EMAIL for backwards compatibility.
|
|
# Set SMTP_TLS_INSECURE=true only for private CA or self-signed certificates.
|
|
# SMTP_TLS controls the TLS mode:
|
|
# - unset / "starttls" (default): plaintext connect, upgrade via STARTTLS.
|
|
# - "implicit" (aliases: "smtps", "ssl"): TLS handshake on connect (SMTPS).
|
|
# Required by providers that only offer port 465 and do not advertise
|
|
# STARTTLS (e.g. Aliyun enterprise mail). Auto-enabled when SMTP_PORT=465
|
|
# and SMTP_TLS is unset.
|
|
# SMTP_EHLO_NAME is the EHLO/HELO name announced to the relay. Defaults to the
|
|
# machine hostname; set a real FQDN when a strict relay (e.g. Google Workspace
|
|
# smtp-relay.gmail.com) rejects the default and the connection drops as an EOF.
|
|
SMTP_HOST=
|
|
SMTP_PORT=25
|
|
SMTP_USERNAME=
|
|
SMTP_PASSWORD=
|
|
SMTP_FROM_EMAIL=
|
|
SMTP_TLS_INSECURE=false
|
|
SMTP_TLS=
|
|
SMTP_EHLO_NAME=
|
|
|
|
# Google OAuth
|
|
# The web login page reads GOOGLE_CLIENT_ID from /api/config at runtime, so
|
|
# changing it only requires restarting the backend / compose stack. No web
|
|
# rebuild is needed.
|
|
GOOGLE_CLIENT_ID=
|
|
GOOGLE_CLIENT_SECRET=
|
|
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_ORIGIN.
|
|
# Set explicitly only when your OAuth callback URL differs from local frontend.
|
|
GOOGLE_REDIRECT_URI=${FRONTEND_ORIGIN}/auth/callback
|
|
|
|
# WeCom smart-bot ("智能机器人" / aibot) integration.
|
|
# MULTICA_WECOM_* drives the bidirectional chat integration (users message
|
|
# the smart bot in their WeCom client, an agent replies over the same
|
|
# WebSocket long connection).
|
|
#
|
|
# MULTICA_WECOM_SECRET_KEY 32-byte base64 master key that encrypts each
|
|
# installation's smart-bot secret at rest.
|
|
# Empty = the wecom integration is disabled
|
|
# and the wecom Web-UI endpoints return 503.
|
|
# Generate with: openssl rand -base64 32
|
|
#
|
|
# MULTICA_WECOM_DEFAULT_LOCALE
|
|
# Which language the bot writes its OWN copy in
|
|
# — binding prompt, "the agent is offline",
|
|
# inbox cards — for readers it cannot look a
|
|
# language up for: a group chat, and anyone who
|
|
# has not linked a Multica account yet. A linked
|
|
# person always overrides this with the language
|
|
# on their Multica profile, so this is the
|
|
# fallback, not a switch that overrides anybody.
|
|
# Accepts zh-Hans (or zh) and en. Empty, or
|
|
# anything else, = zh-Hans, which is what every
|
|
# existing deployment already gets.
|
|
# It does NOT touch the agent's answers: those
|
|
# come back in whatever language the model
|
|
# replied in, the same as today.
|
|
# MULTICA_WECOM_MEDIA_ALLOW_CIDRS
|
|
# Comma-separated ranges the media fetcher may
|
|
# dial even though they look reserved. Empty by
|
|
# default, and that is the right value almost
|
|
# everywhere: the guard exists because a callback
|
|
# hands us a URL somebody else controls, and a
|
|
# redirect to a metadata endpoint is one line of
|
|
# response away.
|
|
# Set it only behind a fake-IP proxy, where every
|
|
# public hostname resolves into the proxy's pool
|
|
# (commonly 198.18.0.0/15) and WeCom's own image
|
|
# host is therefore refused along with everything
|
|
# else. Whatever you list here becomes reachable
|
|
# by a URL WeCom supplies. Loopback, the private
|
|
# ranges and the IPv6 translation prefixes
|
|
# (64:ff9b::/96, 64:ff9b:1::/48, 2002::/16 —
|
|
# IPv4 addresses in an IPv6 spelling) are refused
|
|
# regardless and cannot be opened this way.
|
|
# MULTICA_WECOM_TRACE 1 = log every frame this adapter reads and
|
|
# writes, so a real-device session can be checked
|
|
# against the server afterwards. Empty / anything
|
|
# else = off, which is the right state outside a
|
|
# debugging session.
|
|
# This logs MESSAGE CONTENT: each traced frame
|
|
# carries the first 120 runes of the message body
|
|
# (runes, not bytes, so Chinese text is not cut
|
|
# mid-character). Binding tokens and other
|
|
# token=… query parameters are redacted, and the
|
|
# smart-bot secret is never read, but user text
|
|
# is not — turn it on deliberately and unset it
|
|
# when the session ends. The backend logs a
|
|
# warning on every boot while it is on. Read at
|
|
# boot, so changing it needs a backend restart;
|
|
# who can read the resulting logs and how long
|
|
# they are kept is your log stack's business, see
|
|
# SELF_HOSTING_ADVANCED.md → WeCom frame tracing.
|
|
#
|
|
# Per-installation credentials (bot_id, secret) are NOT env vars — they
|
|
# live in the channel_installation table. Create an installation from the
|
|
# WeCom settings tab in the app (Settings → Integrations → WeCom), which
|
|
# seals the smart-bot secret with MULTICA_WECOM_SECRET_KEY before storing it.
|
|
#
|
|
# SINGLE-REPLICA CONSTRAINT: unlike Slack/Lark (stateless HTTP outbound), the
|
|
# WeCom smart bot's only outbound path is an in-process WebSocket long
|
|
# connection. Agent replies and inbox pushes are delivered only by the backend
|
|
# replica that currently holds a given bot's connection lease. If you run more
|
|
# than one backend replica, responses produced on a replica that does not hold
|
|
# the lease are dropped and the user sees nothing. Until cross-replica outbound
|
|
# routing lands, run the WeCom-enabled backend as a SINGLE replica.
|
|
MULTICA_WECOM_SECRET_KEY=
|
|
MULTICA_WECOM_DEFAULT_LOCALE=
|
|
MULTICA_WECOM_MEDIA_ALLOW_CIDRS=
|
|
MULTICA_WECOM_TRACE=
|
|
|
|
# S3 / CloudFront
|
|
# S3_BUCKET — bucket NAME only (e.g. "my-bucket"). Do NOT include the
|
|
# ".s3.<region>.amazonaws.com" suffix; the server builds the public URL
|
|
# from S3_BUCKET + S3_REGION. S3_REGION must match the bucket's real region.
|
|
S3_BUCKET=
|
|
S3_REGION=us-west-2
|
|
AWS_ACCESS_KEY_ID=
|
|
AWS_SECRET_ACCESS_KEY=
|
|
# AWS_ENDPOINT_URL — optional S3-compatible endpoint (MinIO, RustFS, R2, etc.).
|
|
# For internal Docker/VPC hosts such as http://rustfs:9000, leave
|
|
# ATTACHMENT_DOWNLOAD_MODE=auto or set proxy explicitly so browsers/CLI do
|
|
# not need direct access to the object store.
|
|
AWS_ENDPOINT_URL=
|
|
# S3_USE_PATH_STYLE — optional S3 addressing mode. Empty preserves the default:
|
|
# true when AWS_ENDPOINT_URL is set for MinIO/LocalStack-style endpoints, false
|
|
# for AWS S3. Set to false for providers that require virtual-hosted-style URLs.
|
|
S3_USE_PATH_STYLE=
|
|
ATTACHMENT_DOWNLOAD_MODE=auto
|
|
ATTACHMENT_DOWNLOAD_URL_TTL=30m
|
|
CLOUDFRONT_KEY_PAIR_ID=
|
|
CLOUDFRONT_PRIVATE_KEY_SECRET=multica/cloudfront-signing-key
|
|
CLOUDFRONT_PRIVATE_KEY=
|
|
CLOUDFRONT_DOMAIN=
|
|
# COOKIE_DOMAIN — optional Domain attribute on session + CloudFront cookies.
|
|
# Leave empty for single-host deployments (localhost, LAN IP, or a single
|
|
# hostname) — session cookies become host-only, which is what the browser
|
|
# wants. Set it when the frontend and backend sit on different subdomains of
|
|
# one registered domain (e.g. ".example.com") — there it is REQUIRED, not
|
|
# optional: without it the frontend cannot read the multica_csrf cookie
|
|
# issued by the API host, so every write fails with 403 "CSRF validation
|
|
# failed" while reads keep working.
|
|
# When you do set it, use the NARROWEST parent domain that still covers both
|
|
# hosts (for agent.example.com + api.agent.example.com that is
|
|
# ".agent.example.com", not ".example.com"). This value also scopes the
|
|
# multica_auth session cookie, and the browser sends it to every host under
|
|
# that domain — HttpOnly stops page scripts from reading it, not sibling
|
|
# subdomains from receiving it. Only use this layout when every host under
|
|
# the chosen domain is operated by the same trusted party; otherwise serve
|
|
# the API from the frontend's own origin and leave this empty.
|
|
# Do NOT set it to an IP address: RFC 6265 forbids IP literals in the cookie
|
|
# Domain attribute and browsers silently drop such cookies.
|
|
COOKIE_DOMAIN=
|
|
|
|
# AUTH_TOKEN_TTL — auth token lifetime. Accepts Go duration strings (e.g.
|
|
# "8760h", "720h30m") or plain integer seconds.
|
|
# Default: 2592000 (30 days). Self-hosted deployments on trusted networks can
|
|
# set a longer value to reduce re-authentication frequency.
|
|
# Note: longer TTL = longer exposure window if a cookie is leaked.
|
|
#
|
|
# Sessions slide: once a session drops below HALF this value, the next
|
|
# authenticated request re-issues it with a full TTL again, so an actively
|
|
# used session is not logged out on the anniversary of its login. The bound
|
|
# this puts on idleness is approximate and one-sided — a session last used
|
|
# while it still had more than half its TTL left is NOT extended, so the
|
|
# guaranteed survivable idle gap is TTL/2, not TTL. There is no absolute
|
|
# cap: an account in continuous use stays signed in indefinitely.
|
|
#
|
|
# Minimum: 60 seconds. The renewal cadence is derived as TTL/10, and clients
|
|
# floor what the server sends at 5 seconds; below a 60-second TTL that floor
|
|
# would start deciding the cadence instead of the server, which is how a
|
|
# client ends up checking less often than the session can survive. A value
|
|
# below the minimum is clamped up to it with a warning at startup, rather
|
|
# than silently logging active users out.
|
|
# AUTH_TOKEN_TTL=2592000
|
|
|
|
# Local file storage (fallback when S3_BUCKET is not set)
|
|
LOCAL_UPLOAD_DIR=./data/uploads
|
|
# Derived by Makefile / local scripts from the backend port.
|
|
# Set explicitly only when uploads are served through a different public URL.
|
|
# LOCAL_UPLOAD_BASE_URL=http://localhost:8080
|
|
|
|
# Security
|
|
# Comma-separated list of allowed origins for CORS and WebSocket connections.
|
|
# Defaults to localhost dev origins when unset.
|
|
# Example: CORS_ALLOWED_ORIGINS=https://multica.ai,https://staging.multica.ai
|
|
CORS_ALLOWED_ORIGINS=
|
|
|
|
# ==================== Rate limiting (optional Redis) ====================
|
|
# Per-IP fixed-window rate limiter on the public auth endpoints
|
|
# (/auth/send-code, /auth/verify-code, /auth/google). Backed by Redis.
|
|
# When REDIS_URL is unset the limiter is a no-op (fail-open) and the
|
|
# backend logs "auth rate limiting disabled: REDIS_URL not configured" at
|
|
# startup. All Redis-backed features share this one Redis configuration,
|
|
# including realtime fan-out, channel leases, rate limits, and token caches.
|
|
# REDIS_URL=redis://localhost:6379/0
|
|
# Cluster deployments support only database 0 and sharded realtime relay mode.
|
|
# The server rejects legacy or dual relay mode when this switch is enabled.
|
|
# REDIS_CLUSTER_MODE=false
|
|
|
|
# Common Redis configurations (choose one):
|
|
#
|
|
# 1. Standalone Redis
|
|
# REDIS_URL=redis://:[email protected]:6379/0
|
|
# REDIS_CLUSTER_MODE=false
|
|
#
|
|
# 2. AWS ElastiCache Serverless (single configuration endpoint + required TLS)
|
|
# The rediss:// scheme is required to enable TLS.
|
|
# REDIS_URL=rediss://:[email protected]:6379/0
|
|
# REDIS_CLUSTER_MODE=true
|
|
#
|
|
# 3. Native multi-node Redis Cluster
|
|
# Separate node addresses between @ and / with commas.
|
|
# REDIS_URL=redis://:[email protected]:6379,10.0.0.2:6379,10.0.0.3:6379/0
|
|
# REDIS_CLUSTER_MODE=true
|
|
# Realtime relay requires Redis/Valkey with XTRIM MINID support (Redis 6.2+).
|
|
# Relay retention defaults: 8 shards, 2000 entries per shard, 5m replay, and
|
|
# exact time trimming after 10m. MAXLEN is the immediate operational pressure
|
|
# valve; size it from observed entry bytes, event rate, and the Redis memory
|
|
# budget. MAXLEN, trim, and TTL controls apply consistently to sharded,
|
|
# legacy, and dual mode; replay grace applies to sharded readers.
|
|
# REALTIME_RELAY_STREAM_MAXLEN=2000
|
|
# REALTIME_RELAY_REPLAY_GRACE=5m
|
|
# REALTIME_RELAY_TRIM_HORIZON=10m
|
|
# Stream TTL is a staged, opt-in fallback for volatile-* policies; it evicts a
|
|
# whole stream and does not protect a noeviction instance before expiry.
|
|
# Safe rollout: first deploy this version everywhere with TTL disabled (the
|
|
# default). Then set the flag to true and roll every replica a second time.
|
|
# Safe rollback: set the flag to false and update every replica, then wait for
|
|
# a complete legacy SCAN cycle (large keyspaces may need several maintenance
|
|
# intervals) and verify relay PTTL values are -1 before rolling back the binary.
|
|
# Do not roll directly back while TTL remains enabled.
|
|
# REALTIME_RELAY_STREAM_TTL_ENABLED=false
|
|
# REALTIME_RELAY_STREAM_TTL=15m
|
|
# REALTIME_RELAY_TTL_REFRESH_INTERVAL=30s
|
|
# REALTIME_RELAY_MAINTENANCE_INTERVAL=1m
|
|
# Set to "true" to skip the CLIENT SETNAME handshake on every Redis
|
|
# connection. Required for managed Redis providers that block the CLIENT
|
|
# command (e.g. GCP Memorystore, AWS ElastiCache with restricted ACLs).
|
|
# Default is false (client naming enabled for connection observability).
|
|
# REDIS_DISABLE_CLIENT_NAME=true
|
|
|
|
# ==================== Channel WebSocket leases ====================
|
|
# Multi-replica production should use Redis. The channel supervisor gets its
|
|
# own client/pool from REDIS_URL and fails closed (does not start connections)
|
|
# if Redis, Ping, or the atomic lease scripts are unavailable at startup.
|
|
# PostgreSQL remains available for self-hosted single-node use and rollback.
|
|
# Switch every API replica together; there is intentionally no dual-lock mode.
|
|
# CHANNEL_WS_LEASE_BACKEND=redis
|
|
# REQUIRED operational policy for the global Redis configured by REDIS_URL:
|
|
# `maxmemory-policy noeviction`. Enable HA plus persistence appropriate to the provider, and
|
|
# alert on memory/capacity, evictions (must stay zero), availability, and
|
|
# command latency. Silent lease-key eviction can interrupt channel connections.
|
|
# Required in Redis mode. Use a stable deployment identifier so environments
|
|
# sharing one Redis database never contend on each other's installation IDs.
|
|
# Allowed characters: letters, digits, dot, underscore, hyphen.
|
|
# CHANNEL_WS_LEASE_NAMESPACE=production
|
|
# Lease timing must satisfy: 0 < poll <= renew < TTL.
|
|
# CHANNEL_WS_LEASE_TTL=180s
|
|
# CHANNEL_WS_LEASE_RENEW_INTERVAL=60s
|
|
# CHANNEL_WS_LEASE_POLL_INTERVAL=30s
|
|
# Redis errors retry quickly, but the owner disconnects before its last
|
|
# confirmed TTL (minus the safety margin) to prevent split-brain.
|
|
# CHANNEL_WS_LEASE_ERROR_RETRY_INTERVAL=5s
|
|
# CHANNEL_WS_LEASE_EXPIRY_SAFETY_MARGIN=5s
|
|
# The explicit PostgreSQL rollback backend restores correctness, not the Redis
|
|
# load reduction: foreign owners still cause one SQL acquire attempt per poll.
|
|
# It also uses the timing values above; set TTL=90s, renew=30s, poll=30s if the
|
|
# previous PostgreSQL failover profile is required during rollback.
|
|
|
|
# Max requests per IP per minute. Defaults are 5 for send-code/google
|
|
# and 20 for verify-code.
|
|
# RATE_LIMIT_AUTH=5
|
|
# RATE_LIMIT_AUTH_VERIFY=20
|
|
# Sliding-window budgets for workspace invitation creation. The actor gate is
|
|
# per inviter, the workspace gate is shared by all admins in a workspace, and
|
|
# the recipient gate is shared across workspaces using a SHA-256 email digest.
|
|
# Without Redis these gates use process-local memory; with Redis their budgets
|
|
# are shared across replicas. Set an individual value to 0 to disable that gate.
|
|
# RATE_LIMIT_INVITATION_ACTOR_10M=10
|
|
# RATE_LIMIT_INVITATION_WORKSPACE_24H=50
|
|
# RATE_LIMIT_INVITATION_RECIPIENT_24H=6
|
|
# Comma-separated CIDRs whose X-Forwarded-For the auth limiter is
|
|
# allowed to trust. Empty (default) = never trust XFF, only RemoteAddr.
|
|
# REQUIRED behind a reverse proxy — otherwise every real user shares
|
|
# the proxy IP and the whole deployment lands in one bucket, turning
|
|
# /auth/send-code into 5 req/min site-wide. Use e.g. "127.0.0.1/32,::1/128"
|
|
# for same-host Caddy/Nginx, or the CDN's published ranges for ALB/CF.
|
|
# This is a separate list from MULTICA_TRUSTED_PROXIES above (which
|
|
# governs the autopilot webhook limiter).
|
|
# RATE_LIMIT_TRUSTED_PROXIES=
|
|
|
|
# Realtime metrics endpoint (/health/realtime) access control. See MUL-1342.
|
|
# When unset, the endpoint only serves direct loopback (127.0.0.1 / ::1)
|
|
# callers with no forwarding headers and returns 404 to everything else —
|
|
# safe for local dev. Any deployment behind a reverse proxy (Caddy / Nginx
|
|
# terminating TLS in front of localhost:8080) MUST set this token, since
|
|
# proxied requests look like loopback at the Go layer; with no token, those
|
|
# requests are refused with 404. Pass the token as
|
|
# `Authorization: Bearer <token>`.
|
|
# REALTIME_METRICS_TOKEN=
|
|
|
|
# GitHub App integration (Settings → GitHub "Connect GitHub")
|
|
# Both must be set for the Connect button to enable and for webhooks to be
|
|
# accepted; leave empty to disable the integration. See docs/github-integration.
|
|
# GITHUB_APP_SLUG is the tail of https://github.com/apps/<slug>.
|
|
GITHUB_APP_SLUG=
|
|
GITHUB_WEBHOOK_SECRET=
|
|
# Optional for webhook-only deployments, but required for PR-card CI /
|
|
# mergeability and Settings → Repositories → Choose from GitHub. These
|
|
# credentials let the server authenticate as the App, mint short-lived
|
|
# installation tokens, and enrich the installation row with the real account
|
|
# login immediately.
|
|
# GITHUB_APP_ID is the numeric "App ID" shown on the App's settings page.
|
|
# GITHUB_APP_PRIVATE_KEY is the full PEM block (including BEGIN/END lines)
|
|
# generated under "Private keys" on that same page; preserve newlines.
|
|
# In this file that means wrapping the value in double quotes and keeping
|
|
# the real line breaks - an unquoted multi-line value stops at line one:
|
|
# GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
|
|
# ...
|
|
# -----END RSA PRIVATE KEY-----"
|
|
GITHUB_APP_ID=
|
|
GITHUB_APP_PRIVATE_KEY=
|
|
|
|
# Self-hosted Git provider integration (Settings → Integrations): Forgejo, Gitea,
|
|
# and GitLab. This is a SELF-HOSTED-MULTICA-ONLY feature (not offered on the
|
|
# managed cloud). Two settings gate it, and BOTH are required — connect,
|
|
# webhook, and rotate all check both:
|
|
# 1. MULTICA_VCS_INTEGRATION_ENABLED=true turns the feature on for this
|
|
# deployment. Left unset/false the section is hidden entirely (this is how
|
|
# the managed cloud keeps it off). The self-host docker-compose sets it.
|
|
# 2. MULTICA_VCS_SECRET_KEY — a base64-encoded 32-byte key that encrypts each
|
|
# workspace's access token and webhook secret at rest. Generate one with:
|
|
# openssl rand -base64 32
|
|
# Unlike GitHub there is no App: each workspace connects its own instance URL
|
|
# and access token in the UI, and registers the returned webhook URL/secret on
|
|
# its repo or org webhook settings.
|
|
MULTICA_VCS_INTEGRATION_ENABLED=
|
|
MULTICA_VCS_SECRET_KEY=
|
|
|
|
# Plugins (Settings → Plugins). A plugin's frontend is an artifact its author
|
|
# uploads; Multica stores it and serves it into a sandboxed iframe, and an
|
|
# installation is bound to one immutable version. Hook endpoints and MCP servers
|
|
# stay on the author's own infrastructure. Settings:
|
|
# 1. MULTICA_PLUGIN_SECRET_KEY — a base64-encoded 32-byte key that encrypts
|
|
# `secret` config values at rest, seals short-lived surface launch URLs,
|
|
# and derives per-installation hook signing secrets. Without it, those
|
|
# secret/surface/hook features fail closed; `mpi_` Public API token issuance
|
|
# remains available and does not require hook signing. Generate one with:
|
|
# openssl rand -base64 32
|
|
# 2. MULTICA_PLUGIN_SURFACE_ORIGIN — a dedicated browser origin routed to this
|
|
# backend (for example https://plugins.example.com). It must differ from
|
|
# the app/API origins and carry no app session cookies. The proxy must
|
|
# preserve Host. The backend serves
|
|
# only /plugin-surfaces/* there; a Host check refuses the same URL on the
|
|
# ordinary API origin.
|
|
# 3. MULTICA_PLUGIN_DIR — an absolute path to a directory of plugin folders,
|
|
# each containing a multica.plugin.json. It is the development publish
|
|
# channel: an author iterating on a surface publishes straight from disk
|
|
# instead of zipping and uploading after every edit. It still produces an
|
|
# ordinary immutable version, so there is no second way for plugin code to
|
|
# reach a browser. Leave empty to accept uploads only.
|
|
# 4. MULTICA_PLUGIN_API_URL — the complete, versioned Public API base URL
|
|
# sent to hook servers (for example https://plugin-api.example.com/v1).
|
|
# Route this hostname's /v1/* traffic to the backend. Leave empty to use
|
|
# MULTICA_PUBLIC_URL + /v1, which is convenient for local and self-hosted
|
|
# deployments that serve the Public API on the ordinary API origin.
|
|
MULTICA_PLUGIN_SECRET_KEY=
|
|
MULTICA_PLUGIN_SURFACE_ORIGIN=
|
|
MULTICA_PLUGIN_API_URL=
|
|
MULTICA_PLUGIN_DIR=
|
|
|
|
# Lark / Feishu bot integration (Settings → Integrations "Bind to Lark")
|
|
# Off until MULTICA_LARK_SECRET_KEY is set — a base64-encoded 32-byte key
|
|
# that encrypts each Bot's app secret at rest. Leave empty to disable.
|
|
# Generate one with: openssl rand -base64 32
|
|
MULTICA_LARK_SECRET_KEY=
|
|
# Mainland 飞书 and international Lark are auto-detected per installation
|
|
# (at QR scan) and served side by side — LEAVE THESE EMPTY for normal use.
|
|
# They are optional deployment-wide overrides that force EVERY installation
|
|
# onto one host (a proxy, a mock for tests, or a single-cloud staging
|
|
# setup); HTTP drives outbound Open Platform API calls, CALLBACK the inbound
|
|
# long-conn bootstrap. NOTE: if you previously ran international Lark by
|
|
# setting these to https://open.larksuite.com, the server relabels your
|
|
# existing installs to region=lark on first boot after upgrade, so you can
|
|
# clear these afterwards. See docs/lark-bot-integration.
|
|
MULTICA_LARK_HTTP_BASE_URL=
|
|
MULTICA_LARK_CALLBACK_BASE_URL=
|
|
# Optional fixed HTTP CONNECT proxy URL for Lark/Feishu WebSocket long-conn
|
|
# handshakes. Leave empty to use standard HTTP_PROXY / HTTPS_PROXY / NO_PROXY
|
|
# environment handling.
|
|
MULTICA_LARK_WS_PROXY_URL=
|
|
|
|
# DingTalk bot integration (Settings → Integrations "Bind to DingTalk")
|
|
# Off until MULTICA_DINGTALK_SECRET_KEY is set — a base64-encoded 32-byte key
|
|
# that encrypts each Bot's AppSecret at rest. Leave empty to disable.
|
|
# Generate one with: openssl rand -base64 32
|
|
MULTICA_DINGTALK_SECRET_KEY=
|
|
|
|
# Slack bot integration (Settings → Integrations → Slack)
|
|
# Off until MULTICA_SLACK_SECRET_KEY is set — a base64-encoded 32-byte key
|
|
# that encrypts each installation's bot and app tokens at rest. Leave empty
|
|
# to disable. Generate one with: openssl rand -base64 32
|
|
MULTICA_SLACK_SECRET_KEY=
|
|
|
|
# Telegram bot integration (Settings → Integrations → Telegram)
|
|
# Off until MULTICA_TELEGRAM_SECRET_KEY is set — a base64-encoded 32-byte key
|
|
# that encrypts each Bot's token at rest. Leave empty to disable. Keep it
|
|
# stable: losing or rotating it makes stored Bot tokens unreadable.
|
|
# Generate one with: openssl rand -base64 32
|
|
MULTICA_TELEGRAM_SECRET_KEY=
|
|
|
|
# Frontend
|
|
# Leave empty — auto-derived from page origin in browser, set by Makefile for local dev.
|
|
# NEXT_PUBLIC_API_URL also feeds the Next.js SSR proxy when explicitly set.
|
|
#
|
|
# When you do set it, give the backend's ORIGIN only — scheme + host (+ port),
|
|
# with NO path: https://api.example.com, not https://api.example.com/api.
|
|
# The client appends its own /api, /uploads and /ws prefixes, so a path here is
|
|
# doubled: /api/api/... for every request and 404s for every avatar/attachment.
|
|
# A trailing /api is stripped defensively, but anything else is kept as-is.
|
|
NEXT_PUBLIC_API_URL=
|
|
NEXT_PUBLIC_WS_URL=
|
|
|
|
# Google Analytics (optional) — GA4 Measurement ID, e.g. G-XXXXXXXXXX.
|
|
# Leave empty to keep gtag.js off (the default for local dev and self-hosting).
|
|
# Read by the Next.js server at runtime, so no web rebuild is needed to toggle it.
|
|
GOOGLE_ANALYTICS_ID=
|
|
|
|
# Remote API (optional) — set to proxy local frontend to a remote backend
|
|
# Leave empty to use local backend (localhost:8080)
|
|
# REMOTE_API_URL=https://multica-api.copilothub.ai
|
|
|
|
# ==================== Self-hosting: Control Signups (fixes #930) ====================
|
|
# Set to "false" to restrict new accounts to allowlisted or invited users.
|
|
# A pending, unexpired workspace invitation also permits emails outside either
|
|
# allowlist when ALLOW_SIGNUP=true. Revocation does not remove existing accounts.
|
|
ALLOW_SIGNUP=true
|
|
# The web UI reads ALLOW_SIGNUP from /api/config at runtime, so toggling this
|
|
# only requires restarting the backend / compose stack — not rebuilding web.
|
|
# It is not hot-reloaded.
|
|
|
|
# Optional: Allow emails from these domains without an invitation (comma-separated)
|
|
ALLOWED_EMAIL_DOMAINS=
|
|
|
|
# Optional: Allow these exact email addresses without an invitation (comma-separated)
|
|
ALLOWED_EMAILS=
|
|
|
|
# Set to "true" to disable workspace creation for every caller on this
|
|
# instance (#3433). Operators usually leave this unset, bootstrap the
|
|
# shared workspace, then flip this to "true" and restart so subsequent
|
|
# users join only via invitations and the entire deployment is visible to
|
|
# the platform admin. The web UI reads this from /api/config at runtime,
|
|
# so toggling requires a backend restart but not a frontend rebuild.
|
|
DISABLE_WORKSPACE_CREATION=
|
|
|
|
# Optional: only these exact email addresses may create workspaces
|
|
# (comma-separated, case-insensitive) (#9400). Leave empty to let every
|
|
# signed-in user create workspaces. DISABLE_WORKSPACE_CREATION=true still
|
|
# blocks everyone, including listed users. Other users can sign in and join
|
|
# workspaces they are invited to, but POST /api/workspaces returns 403 for
|
|
# them. The UI does not hide the "Create workspace" entry for them; the
|
|
# server error is shown instead. Listing a user grants no access to other
|
|
# workspaces. Requires a backend restart.
|
|
WORKSPACE_CREATION_ALLOWED_EMAILS=
|
|
|
|
# ==================== Anonymous Self-host Telemetry ====================
|
|
# The API server sends one first-party, deployment-level anonymous snapshot per
|
|
# UTC day to https://telemetry.multica.ai. It contains the release version,
|
|
# bucketed workspace/member/agent/active-daemon counts, and aggregate run counts
|
|
# for the previous 24 hours. It never includes names, email addresses, content,
|
|
# business IDs, host/device data, prompts, paths, credentials, or error text.
|
|
# Set to 1 or true (case-insensitive) and recreate/restart the backend to disable
|
|
# both collection and delivery. This is separate from ANALYTICS_DISABLED below.
|
|
DO_NOT_TRACK=
|
|
|
|
# ==================== Analytics (PostHog) ====================
|
|
# Product analytics events feed the acquisition → activation → expansion funnel.
|
|
# Leave POSTHOG_API_KEY empty for local dev / self-hosted instances; the server
|
|
# will run a no-op analytics client and ship nothing.
|
|
POSTHOG_API_KEY=
|
|
POSTHOG_HOST=https://us.i.posthog.com
|
|
# Optional override for the `environment` PostHog event property.
|
|
# Defaults from APP_ENV and normalizes to production / staging / dev.
|
|
ANALYTICS_ENVIRONMENT=
|
|
# Force the no-op client even when POSTHOG_API_KEY is set (CI / opt-out).
|
|
ANALYTICS_DISABLED=
|
|
|
|
# Server-managed Qoder Cloud Agents connections require
|
|
# MULTICA_VCS_SECRET_KEY: it encrypts the saved Qoder and GitHub tokens
|
|
# and the run journal in the database. Keep it stable across restarts.
|