Configure the Services¶
For an already-current deployment, use Upgrade an existing current deployment for enterprise identity and retrieval migrations. Optional enterprise identity, native providers, and model guardrails have separate setup and restart procedures.
The console and gateway are configured through environment variables. The gateway also loads the global and organization YAML catalog. Use a service manager or secret-injection mechanism that does not place secret values in Git, shell history, process arguments, or generated documentation.
Generate independent secrets¶
Generate separate high-entropy BETTER_AUTH_SECRET for Console authentication and API_KEY_PEPPER for key verification. Generate four distinct transport secrets of at least 32 characters: INTERNAL_SERVICE_SECRET, AGENT_RUNTIME_INBOUND_SECRET, AGENT_RUNTIME_HEARTBEAT_SECRET, MANAGED_EXECUTION_AUTH_MASTER. Gateway receives all four; Console receives only the internal transport secret; runtime receives inbound/heartbeat only; Collector receives internal transport under MISSION_CONTROL_INTERNAL_SERVICE_SECRET. Keep provider/Connector and ClickHouse credentials in their owning service only.
Shared storage settings¶
Working directory for service commands is platform/ unless specified. The Console, the gateway, bootstrap and the command-line tools share one PostgreSQL 16+ database, named by DATABASE_URL. It holds the governance tables and the application content (Agent instructions and Chat transcripts) together. The Postgres superuser creates the database with the libc C collation and character type, owned by a dedicated login role; see Bootstrap and first sign-in. db:migrate refuses any other collation, character type or locale provider, C.UTF-8 included, with storage_collation_refused before it applies anything.
export DATABASE_URL="postgres://mission_control:replace-with-a-generated-password@127.0.0.1:5432/mission_control"
The URL must start with postgres:// or postgresql://. Keep the password in the user part and percent-encode reserved characters; the deployment helper refuses a password= query parameter. The URL is a secret: keep it in the protected service environment (shared.env, mode 600, on the existing VPS). Each process opens a pool of at most 10 connections, so allow max_connections for the gateway, the Console and the tools you run beside them. Point DATABASE_URL at the primary PostgreSQL server itself. Connection poolers such as PgBouncer are not supported: the release is verified only with direct connections. A read replica cannot serve as DATABASE_URL, because the services write to the database. Run one Console against a database, and one or several gateway instances against it behind a load balancer (Run several gateway instances); more than one Console is not supported in this release.
Run pnpm --filter @mission-control/database db:migrate after installing a release and before starting it. The gateway, the Console and connector:rotate-credentials refuse to start with STORAGE_MIGRATION_REQUIRED: run pnpm --filter @mission-control/database db:migrate while any bundled migration is missing, and never migrate on their own. Bootstrap migrates before it writes, and config:check does not check migrations. Changing DATABASE_URL requires restarting every process that reads it.
Inject real secrets through the service manager; do not copy fictional labels as production credentials. Changing API_KEY_PEPPER invalidates keys. Coordinate purpose-secret changes only across affected recipients and drain active work. The gateway-only master must never reach runtime.
Console settings¶
For an existing installation moving to another hostname, follow Change the public domain to coordinate tunnel routes, authentication callbacks, runtime URLs, and client connections.
export NODE_ENV="production"
export APP_URL="https://mission-control.example.test"
export TRUSTED_ORIGINS="https://mission-control.example.test"
export BETTER_AUTH_SECRET="replace-with-a-random-auth-secret"
export BOOTSTRAP_ADMIN_EMAIL="owner@example.com"
export GATEWAY_MANAGEMENT_URL="http://127.0.0.1:4001"
export GATEWAY_PUBLIC_URL="https://gateway.example.test"
export NEXT_PUBLIC_DOCS_URL="https://docs.example.test"
export AUTH_RATE_LIMIT_MAX="20"
export MAGIC_LINK_RATE_LIMIT_MAX="5"
export MAIL_TRANSPORT="smtp"
export SMTP_HOST="smtp.example.test"
export SMTP_PORT="587"
export SMTP_SECURE="false"
export SMTP_USER="mission-control"
export SMTP_PASSWORD="replace-with-an-smtp-password"
export MAIL_FROM="Mission Control <mission-control@example.test>"
APP_URL is used in authentication and invitation links. TRUSTED_ORIGINS is a comma-separated allowlist; include only origins that legitimately host the console. The bootstrap email remains eligible to request a magic link even before a membership lookup succeeds. GATEWAY_MANAGEMENT_URL is the private control-listener origin the console signs its configuration, health, heartbeat, and telemetry-control calls to. GATEWAY_PUBLIC_URL is the inference-listener origin used for /v1/chat/completions, /v1/responses, /v1/models, /v1/embeddings, /v1/rerank, /mcp, and /v1/deployments/{deploymentId}:invoke, and is the origin shown in Connect snippets. Never expose the control listener publicly. NEXT_PUBLIC_DOCS_URL is optional and is embedded in the browser bundle at build time; changing it requires a new console build.
SMTP authentication is optional only when the SMTP server explicitly permits unauthenticated delivery. SMTP_SECURE=true selects implicit TLS; port 587 commonly uses false with transport upgrade managed by the SMTP library and server.
GitHub sign-in is optional. Add the pair below to put Continue with GitHub on the sign-in page:
export GITHUB_CLIENT_ID="Iv1.replace-with-the-oauth-app-client-id"
export GITHUB_CLIENT_SECRET="replace-with-the-oauth-app-client-secret"
Create the OAuth app under Settings > Developer settings > OAuth Apps in GitHub. Set its homepage to APP_URL and its authorization callback to APP_URL plus /api/auth/callback/github — for the values above, https://mission-control.example.test/api/auth/callback/github. The console requests only the read:user and user:email scopes, which it uses to read the account's primary verified address. Omit either variable and the button stays hidden, leaving magic links as the only way in; the pair takes effect on restart.
GitHub does not widen who may sign in. The console admits a GitHub account only when its primary email address is verified on GitHub and already belongs to a member, an unexpired pending invitation, or BOOTSTRAP_ADMIN_EMAIL; anything else is returned to the sign-in page unauthenticated. The bootstrap owner must sign in once with a magic link before GitHub will attach to that account, because linking a social account requires an address the console has already verified.
For local development only:
export NODE_ENV="development"
export MAIL_TRANSPORT="console"
export MAIL_OUTBOX_PATH="/tmp/mission-control-mail.jsonl"
Console mail writes magic and invitation links to standard output or the optional JSON-lines outbox. The application rejects this transport when NODE_ENV=production.
Gateway settings¶
export MISSION_CONTROL_CONFIG="/srv/mission-control/config/mission-control.yaml"
export INFERENCE_GATEWAY_HOST="127.0.0.1"
export INFERENCE_GATEWAY_PORT="4000"
export CONTROL_GATEWAY_HOST="127.0.0.1"
export CONTROL_GATEWAY_PORT="4001"
export MCP_ALLOWED_ORIGINS="https://agent-app.example.test"
export AGENT_ALLOWED_ORIGINS="https://storefront.example.test"
export AGENT_RUNTIME_URL="http://127.0.0.1:4100"
export GATEWAY_DRAIN_SECONDS="10"
export GATEWAY_SHUTDOWN_TIMEOUT_SECONDS="60"
export LOG_LEVEL="info"
The gateway resolves designated provider credential, Connector auth.credential, nested header credential, and OAuth clientSecret references before startup. Only env/NAME is supported; cloud secret stores are unavailable. All required values must be nonempty, and an invalid file or unresolved required reference refuses startup before either listener binds. Each reference must name a variable the Organization owns: a reference to a platform setting, a cloud SDK identity variable or a reserved namespace such as MISSION_CONTROL_ or AWS_ refuses startup with Secret reference names a reserved platform environment variable and the file and field (see Names Organization YAML cannot reference). Ordinary headers and text, including values resembling env/NAME, stay literal and do not trigger secret lookup. For example:
export ACME_OPENAI_API_KEY="replace-with-a-provider-key"
export ACME_CRM_MCP_TOKEN="replace-with-an-mcp-token"
If any referenced variable is absent or empty, configuration validation fails. Secret values are held in the gateway runtime snapshot; the stored and displayed catalog records only whether each reference resolved.
MCP_ALLOWED_ORIGINS is a comma-separated exact allowlist for browser POST /mcp and OPTIONS /mcp. An empty value permits no browser origins; wildcards are not supported. A preflight succeeds only for POST and the documented MCP/authorization/trace request headers, including valid Mcp-Param-*; other origins, methods, or headers fail closed. Non-browser service clients may omit Origin when their deployment policy allows it. Changing the value requires a gateway restart, no new key, and no new MCP session because revision 2026-07-28 is stateless.
AGENT_ALLOWED_ORIGINS is the exact-match browser allowlist for public deployment invoke requests carrying an Origin header. Console Chat is proxied by the console server and sends no Origin; leave the list empty until an external UI needs it. AGENT_RUNTIME_URL points at the private Agent runtime service; the inference gateway sends one signed execution envelope and preserves the same W3C Trace context across the streamed response.
Shared MCP OAuth¶
Run commands from platform/ with the gateway's service environment. Set the same public APP_URL on Console and gateway, and register APP_URL plus /api/connectors/oauth/callback at the provider. Keep gateway /internal/connectors/.../oauth/* private. Omit or redact callback queries in reverse-proxy access logs and analytics.
Generate a keyring value for protected gateway environment storage:
python3 -c 'import base64,json,secrets; print(json.dumps({"key_2026_09": base64.b64encode(secrets.token_bytes(32)).decode()}))'
Store that JSON in MCP_CREDENTIAL_ENCRYPTION_KEYS and set MCP_CREDENTIAL_ACTIVE_KEY_ID=key_2026_09. Both are gateway-only; do not give them to Console or Agent runtime. Set referenced OAuth client secrets separately. Back up the keyring independently from the database, run preflight after changes, then restart. Missing keys fail startup preparation; there is no plaintext fallback. Static-only deployments may omit the keyring.
MCP_AUTH_ALLOWED_PRIVATE_ORIGINS optionally lists exact comma-separated HTTPS authorization origins approved by deployment. An origin must also be allowed by the Connector. It permits only RFC1918 IPv4 or ULA IPv6 destinations; loopback, link-local metadata, IPv4-mapped IPv6 and other special-use destinations stay blocked. Configure egress controls too. Changes require a gateway restart and never expand a connector's origin list.
Authentication requests are bounded to ten seconds, 1 MiB decompressed bodies and two approved metadata redirects. Credential-bearing POSTs never follow redirects. See the task guide and key rotation.
Agent runtime service¶
The private PydanticAI runtime executes admitted Preview, Chat, production-test and public API turns. Working directory: runtime/agent-runtime/. Use Python 3.12–3.14 and the locked environment.
uv sync --locked --all-groups
export MISSION_CONTROL_GATEWAY_URL="http://127.0.0.1:4000"
export MISSION_CONTROL_CONTROL_URL="http://127.0.0.1:4001"
export AGENT_RUNTIME_HOST="127.0.0.1"
export AGENT_RUNTIME_PORT="4100"
export MISSION_CONTROL_OTLP_ENDPOINT="https://telemetry.example.test/v1/traces"
export MISSION_CONTROL_OTLP_PROTOCOL="http/protobuf"
uv run --locked mission-control-agent-runtime
Before starting, inject matching AGENT_RUNTIME_INBOUND_SECRET and AGENT_RUNTIME_HEARTBEAT_SECRET; do not set an internal/global fallback or derivation master. Runtime has no caller/provider/Connector key. It receives exact admitted turn authority, preserves W3C context and uses a separate OTLP capability. Managed model rounds have a 4,096 output-token bound. Verify private http://127.0.0.1:4100/healthz; provider and telemetry delivery require separate functional checks.
Standard OTLP services¶
Collector needs its internal transport secret, listener TLS and memory settings. Its mc_producer/mc_writer pipeline forwards synchronously to the gateway writer and keeps no persisted credential queue or ClickHouse credentials. Gateway holds writer/cleanup database identities; Console holds the read-only identity and separately configured deletion identity. Team serving storage is team_span_payloads, team_span_heads, team_span_deletions.
See telemetry operations and environment variables. External user/service exporters send a key with telemetry.ingest per export; managed runtime receives only its dedicated capability.
Coordinated activation¶
For a current installation, back up and upgrade without resetting current data. An installation that still keeps its data in SQLite files first follows Migrate from SQLite. No normal startup or deployment command performs a reset.
- Migrate the database with
db:migrateand apply the complete current ClickHouse schema chain. - Bootstrap canonical Organization/owner/Team state before loading its YAML mapping.
- Start gateway with both listeners and reviewed configuration.
- Start compatible Agent runtime, Collector and Console with purpose-separated secrets.
- Verify private
/readyz, then a bounded permitted request and a denied request. Open the actual Team evidence before restoring traffic.
Private readiness becomes ready only after matching mission-control.trace-native/v3 heartbeats. Inference has no health/control endpoint.
curl --fail http://127.0.0.1:4001/readyz
curl --output /dev/null --write-out '%{http_code}\n' http://127.0.0.1:4000/v1/models
Expected: ready JSON from private control and 401 without an inference key.
Restart behavior¶
Environment-variable changes require a restart of the affected process. INTERNAL_SERVICE_SECRET rotation requires matching Console, gateway and Collector values; runtime purpose-secret changes require matching gateway/runtime restarts. Master rotation invalidates old managed proofs. API_KEY_PEPPER rotation restarts Console and gateway and invalidates every key. MCP_ALLOWED_ORIGINS changes restart gateway but require no new key. Collector listener/TLS/writer changes restart Collector; ClickHouse HTTP credentials restart Console; server-side role/encryption changes require corresponding ClickHouse operations.
YAML and required static-secret changes require a coordinated gateway/Console restart with the reviewed files. Running processes ignore later file edits. Key lifetimes apply to subsequent issuance/rotation, while configured concurrency and base capture settings apply after restart; existing keys otherwise remain valid. A restart drains: the instance answers draining on its readiness probes for GATEWAY_DRAIN_SECONDS, then closes its listeners and finishes the requests in flight (Health and logging); with several instances, restart them one at a time (Run several gateway instances). Console displays current redacted configuration only and provides no restart or Apply controls. Optional preflight from platform/ uses the same startup preparation without writes or network calls:
Bootstrap organizations first and supply the deployment's DATABASE_URL, configuration paths and required environment. Strict YAML, reference/profile, organization and static-secret errors exit nonzero before serving; a successful preflight exits zero. It does not probe model/MCP servers or complete OAuth consent. MCP_DISCOVERY_INTERVAL_SECONDS controls one gateway-owned refresh (default 300 seconds, 0 disables); retired MCP_HEALTH_INTERVAL_SECONDS is rejected with a migration diagnostic. PydanticAI OTLP changes require a new external process.
Budget policy changes are database-backed console operations. They require no environment variable, YAML changes, process restart, new key, or new session and apply on the next governed admission. Configure model input/output prices and supported per-tool prices in Organization YAML. Finite spending caps fail closed on unbounded/unknown liability; generation callers need a finite output bound.
See the complete environment-variable reference.