Environment variables
Every variable the gateway reads, with its default and what it controls.
The gateway validates its environment at startup and exits if a required variable is missing or malformed. Configure the five required values below, then review optional settings for your deployment. Gateway and dashboard processes read separate environment files.
Required
| Variable | Format | Notes |
|---|---|---|
MASTER_KEY | String, 32+ chars | Full-access operator key. Generate with openssl rand -base64 48. |
ENCRYPTION_KEYRING | JSON object | Maps key ids to 64-character hex AES keys. Keep old ids during rotation. |
ACTIVE_ENCRYPTION_KEY_ID | Key id | Must exist in ENCRYPTION_KEYRING; encrypts every new envelope. |
DATABASE_URL | Postgres connection URL | |
REDIS_URL | Redis connection URL |
Server
| Variable | Default | Controls |
|---|---|---|
PORT | 4000 | HTTP listen port. |
TRUSTED_PROXY_HOPS | 0 | Number of rightmost, operator-controlled proxy hops trusted in X-Forwarded-For. 0 ignores the header. |
PUBLIC_MODELS_RPM | 600 | Per-process anonymous /v1/models requests allowed each minute per resolved client IP. 0 disables the limit. |
NODE_ENV | development | development | test | production. |
LOG_LEVEL | info | debug | info | warn | error. |
MIGRATE_ON_BOOT | true | Apply pending migrations before the server listens. Every replica does it behind an advisory lock, so they serialise; a failure stops the process. |
DRAIN_DELAY_MS | 15000 (0 outside production) | How long /health/ready answers 503 while the process keeps serving, so the proxy can deregister it before the listener closes. See Rollouts. |
SHUTDOWN_TIMEOUT_MS | 120000 | Grace for in-flight requests once the listener is closed. Must cover the longest streamed response. |
Requests, logging, and limits
| Variable | Default | Controls |
|---|---|---|
OBSERVABILITY_PAYLOAD_ACCESS | open | Who may read a retained sample. open: a role with payloads:read, audited. sealed: nobody, through any credential — samples are still captured and encrypted, and the refused attempt is audited. |
OBSERVABILITY_PAYLOAD_RETENTION_DAYS | 7 | Encrypted forensic payload retention. |
OBSERVABILITY_METADATA_RETENTION_DAYS | 30 | Retention for completed operation and upstream-attempt metadata. |
ADMIN_AUDIT_RETENTION_DAYS | 365 | Retention for the /admin audit trail. Outlives operation metadata: one row per configuration change, asked about months later. |
OBSERVABILITY_PAYLOAD_MAX_BYTES | 32768 | Maximum size of each request, response, error, and attempt component in an encrypted sample after redaction. |
IMAGES_MAX_MULTIPART_BYTES | 805000000 (~805 MB) | Aggregate cap on an image multipart upload (images + mask + fields), streamed to temporary disk. |
AUDIO_MAX_MULTIPART_BYTES | 30000000 (30 MB) | Aggregate cap on an audio transcription multipart upload. |
RESPONSES_STORE_DEFAULT | true | Default for store when a /v1/responses request omits it. Set false for privacy-first deployments. |
RESPONSES_STATE_RETENTION_DAYS | 14 | How long a stored Response (with store: true) stays retrievable via GET /v1/responses/:id. |
RESPONSES_WEBSOCKET_MAX_CONNECTIONS | 1000 | Maximum live Responses WebSocket connections per gateway process. |
RESPONSES_WEBSOCKET_MAX_CONNECTIONS_PER_KEY | 20 | Maximum live Responses WebSocket connections for one virtual key (the master key has its own scope). |
RESPONSES_WEBSOCKET_MAX_QUEUED_TURNS | 64 | Maximum queued response.create turns per connection; one turn is processed at a time. |
RESPONSE_STATE_GC_INTERVAL_MS | 3600000 (1h) | How often the in-process job deletes expired response state. |
Runtime extensions
| Variable | Default | Controls |
|---|---|---|
EXTENSIONS_MAX_FAILURES | 3 | Consecutive hook failures before an extension instance is disabled for that process. |
EXTENSIONS_RELOAD_INTERVAL_MS | 15000 | How often each replica polls for a changed extension registry to hot-reload. |
EXTENSIONS_MAX_CODE_BYTES | 1000000 (1 MB) | Maximum size of an uploaded extension module's source. |
EXTENSIONS_HOOK_TIMEOUT_MS | 5000 | Per-hook wall-clock budget. 0 disables the timeout. An exceeded hook is aborted and counts as a failure. |
Dashboard authentication
Optional, and off by default. When disabled the gateway never mounts /auth/* or /admin/users, and
/admin accepts only the master key — exactly as before this surface existed. The tables are created
either way and simply stay empty.
| Variable | Default | Controls |
|---|---|---|
DASH_ENABLED | false | Master switch. Enabling it requires DASH_ROOT_USER; the process refuses to start otherwise. |
DASH_ROOT_USER | — | Root operator's username. Read from the environment, never stored in the database, so it cannot be disabled, demoted, or deleted. |
DASH_ROOT_PASSWORD | falls back to MASTER_KEY | Root operator's password. |
Session lifetime, the idle window and the login lockout are not environment variables: they
are policy an owner edits from Settings › Operator sessions, or through
PUT /admin/dashboard-settings. Changing them takes effect within seconds, without a redeployment,
and the change is audited like any other.
Set DASH_ROOT_PASSWORD in production rather than relying on the fallback: MASTER_KEY and the
dashboard password have different rotation cycles, and sharing one value means rotating the API
credential logs every operator out. See Security.
Dashboard process
Read by the dashboard container, not by the gateway. The full list is
apps/dashboard/.env.example;
the documentation site needs nothing at all. Nothing is baked into its image, so all of
them take effect on restart and one image tag can be promoted between environments unchanged.
| Variable | Default | Controls |
|---|---|---|
GATEWAY_URL | — | Where the gateway is. Required. Only this container needs to reach it — the browser never leaves the dashboard's own origin, so the gateway needs no public domain. |
PORT | 3001 | Listen port (DASH_PORT in development). |
The dashboard checks the address before it accepts its first request, so a missing one is a container that exits with the reason printed rather than one that starts and then fails every page.
DASH_ENABLED is read by the gateway — it gates /auth/* and /admin/users there. The Compose
files in this repository reuse it as a single switch for the pair, so the dashboard container exits
cleanly when it is false.
Observability (OpenTelemetry)
| Variable | Default | Controls |
|---|---|---|
OTEL_ENABLED | false | Master switch. Metrics/traces are otherwise not exported at all. |
OTEL_SERVICE_NAME | bifrost | Service name attached to exported telemetry. |
OTEL_METRIC_EXPORT_INTERVAL_MS | 60000 | How often metrics are pushed to the collector. |
These are read by the gateway's own schema. Once OTEL_ENABLED=true, the exporters (OTLPMetricExporter,
OTLPTraceExporter) also read the standard OpenTelemetry SDK environment variables directly —
notably OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS — which are not gateway-specific
and so aren't validated by the schema above. See Observability for a working
example.
Boolean variables (RESPONSES_STORE_DEFAULT, OTEL_ENABLED, DASH_ENABLED) accept
1/true/yes/on or 0/false/no/off, case-insensitively.
Next steps
- Setup — the minimal set to get running.
- Security — credential and observability encryption boundaries.
- Observability — OTEL setup end to end.
- Production checklist — what to double-check before going live.