# Environment variables

> Every variable the gateway reads, with its default and what it controls.

URL: https://boelabs.com/en/docs/ai-gateway/reference-environment
Language: English

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 | TLS follows `sslmode` as in libpq; without it the connection is plaintext. |
| `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](https://boelabs.com/docs/ai-gateway/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. |
| `VIRTUAL_KEY_SECRET_ACCESS` | `open` | Who may copy a virtual key again. `open`: a role with `keys:write`, audited. `sealed`: nobody, through any credential — stored keys stay encrypted, and the refused attempt is audited. See [Virtual keys](https://boelabs.com/docs/ai-gateway/virtual-keys#copy-a-key-again). |
| `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). |
| `REALTIME_WEBSOCKET_MAX_CONNECTIONS` | `500` | Maximum live `/v1/realtime` transcription sessions per gateway process. |
| `REALTIME_WEBSOCKET_MAX_CONNECTIONS_PER_KEY` | `20` | Maximum live `/v1/realtime` transcription sessions 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](https://boelabs.com/docs/ai-gateway/security#dashboard-authentication).

## Dashboard process

Read by the dashboard container, not by the gateway. The full list is
[`apps/dashboard/.env.example`](https://github.com/boelabs/ai-gateway/blob/main/apps/dashboard/.env.example);
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. |
| `DOCS_URL` | — | Where operators read the documentation, such as `https://boelabs.com/docs/ai-gateway`. Optional; with it the sidebar and the command palette link to it, opening in a new tab. |
| `PORT` | `3001` | Listen port (`DASH_PORT` in development). |

The dashboard checks the addresses before it accepts its first request, so a missing `GATEWAY_URL`,
or a `DOCS_URL` that is not an http(s) address, 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` | `ai-gateway` | 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](https://boelabs.com/docs/ai-gateway/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](https://boelabs.com/docs/ai-gateway/setup) — the minimal set to get running.
- [Security](https://boelabs.com/docs/ai-gateway/security) — credential and observability encryption boundaries.
- [Observability](https://boelabs.com/docs/ai-gateway/observability) — OTEL setup end to end.
- [Production checklist](https://boelabs.com/docs/ai-gateway/production-checklist) — what to double-check before going live.

