Environment Variables Reference
All environment variables are validated at startup using Zod schemas defined in packages/server/src/config/env.ts. The server will exit with descriptive errors if validation fails.
Core
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV | No | development | development, production, or test |
PORT | No | 3000 | HTTP server listen port |
HOST | No | 0.0.0.0 | HTTP server bind address |
LOG_LEVEL | No | info | error, warn, info, http, verbose, debug, silly |
Database
| Variable | Required | Default | Validation |
|---|---|---|---|
MONGODB_URI | No | mongodb://localhost:27017/ai-gateway | Must be a valid URL. See the note below on replica sets. |
MONGODB_DB_NAME | No | ai-gateway | Non-empty string |
REDIS_HOST | No | localhost | String |
REDIS_PORT | No | 6379 | Positive integer |
REDIS_PASSWORD | Prod | (empty) | Required in production |
REDIS_DB | No | 0 | Non-negative integer |
REDIS_SENTINELS | No | (empty) | host:port, comma-separated. Set it and the connection follows the master Sentinel elects, ignoring REDIS_HOST/REDIS_PORT. Empty means connect directly. |
REDIS_SENTINEL_MASTER | No | aigw-redis | Must match the name in sentinel monitor <name> |
REDIS_SENTINEL_PASSWORD | No | (empty) | Sentinel’s own requirepass, when it differs from the data password |
Authentication
| Variable | Required | Default | Validation |
|---|---|---|---|
JWT_SECRET | Yes | — | Minimum 16 characters. Must not contain placeholder values in production. |
JWT_REFRESH_SECRET | Yes | — | Minimum 16 characters. Must not contain placeholder values in production. |
JWT_ACCESS_EXPIRY | No | 15m | Duration string (e.g., 15m, 1h) |
JWT_REFRESH_EXPIRY | No | 7d | Duration string (e.g., 7d, 30d) |
ENCRYPTION_KEY | Yes | — | Exactly 64 hex characters (256-bit AES key). Sequential patterns rejected in production. |
Super Admin Seed
Used by pnpm seed to create the initial administrator account.
| Variable | Required | Default | Validation |
|---|---|---|---|
SUPER_ADMIN_EMAIL | No | admin@ai-gateway.local | Valid email address |
SUPER_ADMIN_PASSWORD | Yes | — | 12+ characters, must include uppercase, lowercase, digit, and special character |
Semantic Cache
| Variable | Required | Default | Description |
|---|---|---|---|
CACHE_EMBEDDING_MODEL | No | nomic-embed-text | Embedding model for cache similarity |
CACHE_SIMILARITY_THRESHOLD | No | 0.96 | Cosine similarity threshold (0.0-1.0) |
CACHE_DEFAULT_TTL_SECONDS | No | 86400 | Cache entry TTL in seconds (default: 24 hours) |
Rate Limiting
| Variable | Required | Default | Description |
|---|---|---|---|
RATE_LIMIT_WINDOW_MS | No | 60000 | Window duration in milliseconds |
RATE_LIMIT_MAX_PER_WINDOW | No | 60 | Max requests per window, per IP, on /v1/* and the public auth routes |
ADMIN_RATE_LIMIT_MAX_PER_WINDOW | No | 600 | Max requests per window on the admin API, counted per operator rather than per IP |
The admin console is a different traffic shape from the gateway: one page view
is ten to twenty API calls, and an operator moving around the console does that
several times a minute. It is therefore counted per signed-in operator, not per
IP — otherwise every operator behind one office NAT shares a single budget, and
ordinary browsing produces 429s that pages can render as empty collections
rather than as failures.
Raise ADMIN_RATE_LIMIT_MAX_PER_WINDOW if operators see panels that fail to
load under normal use. /api/admin/auth/* and the SSO routes are deliberately
excluded and keep the per-IP limit, because that is where brute-force
protection belongs.
CORS and Networking
| Variable | Required | Default | Description |
|---|---|---|---|
CORS_ORIGINS | No | (empty) | Comma-separated allowed origins (each must be a valid URL) |
SOCKET_CORS_ORIGIN | No | http://localhost:3001 | The origin the admin console is served from. The console opens a WebSocket for live dashboard and audit updates; if this does not match, the connection is refused and the console simply stops updating without reporting why. |
ADMIN_URL | No | http://localhost:3001 | Admin dashboard URL |
SSO_CALLBACK_BASE_URL | Set it in production | (falls back to ADMIN_URL) | The gateway’s public origin, which is what the redirect URI registered with your identity provider must match. Taking it from the request instead would be a Host-header injection, so it is never inferred in production. The Settings page shows the resulting URIs — copy them from there rather than assembling them. |
TRUST_PROXY | Behind a proxy | (trust nothing) | How many reverse proxies sit in front of the gateway. See below — this decides who every caller appears to be. |
TRUST_PROXY
The gateway identifies a caller by the address the connection came from. When it
sits behind nginx, an ingress controller or a load balancer, that address is the
proxy’s, and the caller’s real one arrives in the X-Forwarded-For header — which
the gateway ignores until you tell it how far to trust.
Four things read the caller’s address:
- API-key IP allow-lists (
allowedIps) compare against it. - The per-IP rate limit on
/api/admin/auth/*and the SSO routes, which is the deployment’s brute-force protection. - The audit trail, which records it as the actor’s address.
- The session list on each operator’s profile page.
Left unset behind a proxy, all four see the proxy’s address on every request: an allow-list compares against a constant, every client in the world shares one rate-limit bucket, and the audit trail records a single address for every actor. The gateway warns about this at boot in production, because nothing else in the log mentions it.
| Value | Meaning |
|---|---|
(unset), false, 0 | Trust nothing. Correct only when the gateway is exposed directly. |
1 | One proxy. This is the bundled docker-compose.prod.yml and both swarm stacks. |
2 | Two — e.g. a CDN in front of your own nginx. Count everything that appends to X-Forwarded-For. |
10.0.0.0/8, 10.1.2.3 | Trust these proxy addresses or CIDRs specifically. |
true | Trust the whole chain. Avoid. |
Count hops rather than using true. A hop count is applied from the connection
inwards, so an entry the client invented is never the one that wins. true takes
the left-most entry instead — which is whatever the caller chose to send, making
an IP allow-list satisfiable with a single header. Counting too high has the
same effect, so raise the number only for proxies that genuinely exist.
Background Workers
| Variable | Required | Default | Description |
|---|---|---|---|
BULLMQ_PREFIX | No | aigw | Redis key prefix for BullMQ queues |
S3 Backup
All S3 variables are optional. When not configured, audit log backups are stored locally.
| Variable | Required | Default | Description |
|---|---|---|---|
S3_BUCKET | No | — | S3 bucket name |
S3_REGION | No | us-east-1 | AWS region |
S3_ACCESS_KEY_ID | No | — | AWS access key ID |
S3_SECRET_ACCESS_KEY | No | — | AWS secret access key |
S3_PREFIX | No | backups | Key prefix within the S3 bucket |
SIEM Export
All SIEM variables are optional. When not configured, SIEM export is disabled.
| Variable | Required | Default | Description |
|---|---|---|---|
SIEM_TYPE | No | — | Export type: splunk, elk, or webhook |
SIEM_URL | No | — | SIEM endpoint URL |
SIEM_TOKEN | No | — | Authentication token |
SIEM_BATCH_SIZE | No | 100 | Events per batch |
Audit retention
Set on the worker, which is the only writer of audit entries.
| Variable | Required | Default | Description |
|---|---|---|---|
AUDIT_LOG_RETENTION_DAYS | No | 365 | Days an audit entry is kept before the TTL index removes it. 0 disables expiry entirely. |
Observability (OpenTelemetry)
All optional. When OTEL_EXPORTER_OTLP_ENDPOINT is unset, OpenTelemetry is a no-op with zero overhead. When set, the server and worker export traces, metrics, and logs over OTLP/HTTP; the collector base URL has /v1/{traces,metrics,logs} appended. See Observability for the full metric list and Grafana/Prometheus assets.
| Variable | Required | Default | Description |
|---|---|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT | No | — | OTLP collector base URL (e.g. http://otel-collector:4318). Unset disables OpenTelemetry export. |
OTEL_SERVICE_NAME | No | ai-gateway / ai-gateway-worker | Overrides the service.name resource attribute (worker defaults to ai-gateway-worker). |
OTEL_METRIC_EXPORT_INTERVAL_MS | No | 30000 | Metric export interval in ms when OTLP is enabled. |
For full ESM auto-instrumentation, start each process with node --import ./dist/telemetry/otel.js ./dist/index.js. Prometheus metrics remain available at GET /metrics regardless of OTLP configuration.
Encryption Key Source (KMS / BYOK)
| Variable | Required | Default | Description |
|---|---|---|---|
KEY_PROVIDER | No | env | Source of the data-encryption key. env uses ENCRYPTION_KEY. A misconfigured value fails loudly rather than silently falling back. (vault / awskms are extension points, not yet implemented.) |
Budget Alert Delivery
All optional; a budget’s notifyVia channel is a no-op unless the matching variables are set. Delivery is best-effort and isolated per channel. See Budgets.
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST | No | — | SMTP server host (enables email alerts). |
SMTP_PORT | No | 587 | SMTP port. |
SMTP_SECURE | No | false | Use TLS on connect (set true for port 465). |
SMTP_USER | No | — | SMTP username. |
SMTP_PASS | No | — | SMTP password. |
SMTP_FROM | No | gatewyse@localhost | From address for alert emails. |
BUDGET_ALERT_WEBHOOK_URL | No | — | Webhook endpoint for budget alerts (HMAC-signed when a secret is set). |
BUDGET_ALERT_WEBHOOK_SECRET | No | — | Secret used to HMAC-sign webhook payloads. |
SLACK_WEBHOOK_URL | No | — | Slack incoming-webhook URL for budget alerts. |
License (EE)
The Enterprise Edition gates service on an Ed25519-signed license JWT issued by the 137 & Co. platform. If LICENSE_TOKEN is missing or invalid when NODE_ENV=production, the server still boots but locks out: every request except health probes, admin sign-in, and the licence endpoints is refused until a valid licence is installed. It never exits, so recovery never requires shell access.
| Variable | Required | Default | Description |
|---|---|---|---|
LICENSE_TOKEN | Prod | — | Ed25519-signed JWT issued by the 137 & Co. platform. Required in production. |
LICENSE_PUBLIC_KEYS | When LICENSE_TOKEN is set | — | One or more PEM-encoded public keys joined with ;; (the double-semicolon delimiter is used because newlines inside env values are unreliable across shells and Docker). |
LICENSE_SERVER_URL | No | — | Points the daily revocation check at the licence server; set it on every non-air-gapped deployment. Air-gap is a signed licence claim (Government edition), never a configuration setting — unsetting this does not make a deployment air-gapped. |
LICENSE_PHONE_HOME_INTERVAL_MS | No | 86400000 | Polling interval (ms) for the revocation check. Default is 24 hours. |
LICENSE_GRACE_PERIOD_MS | No | 604800000 | How long a revoked licence continues to serve before the deployment locks out. Default is 7 days, measured from the issuer’s revocation timestamp. |
GATEWYSE_LICENSE_FINGERPRINT | No | — | Optional hardware-fingerprint override. Read directly via process.env in packages/license/src/fingerprint.ts (it is not part of the Zod env schema), because the license verifier runs before env validation. When set, it overrides the auto-derived machine fingerprint used for hardware-bound licenses. |
The license verifier runs before the Zod env validator and Winston logger because both depend on environment that has not yet been validated at bootstrap time. For that reason LICENSE_* and GATEWYSE_LICENSE_FINGERPRINT are read straight from process.env and do not appear in the Zod env schema. See packages/server/src/services/license.service.ts for the boot sequence.
Observability
| Variable | Required | Default | Description |
|---|---|---|---|
METRICS_TOKEN | No | (unset) | When set, GET /metrics requires Authorization: Bearer <token> and answers 401 without it. Minimum 16 characters. |
/metrics is mounted ahead of all authentication, because a Prometheus
scraper is not a session and cannot refresh a credential. It publishes
per-provider and per-model spend and token totals, so it should not be
reachable from outside the deployment.
The bundled nginx returns 404 for /metrics, so the Docker Compose topology
is already closed. The Kubernetes manifests expose the server directly — set
METRICS_TOKEN there, or keep the path off the ingress. In production the
server logs a warning at boot when it is unset, because nothing else in the log
mentions the endpoint either way.
Add it to a Prometheus scrape config with:
scrape_configs: - job_name: gatewyse authorization: credentials: <token> static_configs: - targets: ['gatewyse:3000']API Documentation
| Variable | Required | Default | Description |
|---|---|---|---|
ENABLE_SWAGGER | No | false | When true, exposes the Swagger UI at /docs. In production this is off by default; set to true to enable. |
Provider API Keys
Provider API keys are managed through the admin dashboard (or the admin API) and stored in the deployment provider catalogue, encrypted at rest with AES-256-GCM — the gateway resolves them from the database at request time. There is no runtime code path that reads OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. from the environment for live routing.
The variable names below are listed for reference only (e.g. for local development scripts, seeding, or smoke tests); setting them does not configure a provider for the gateway:
OPENAI_API_KEY=sk-...ANTHROPIC_API_KEY=sk-ant-...GOOGLE_AI_API_KEY=AIza...AZURE_OPENAI_API_KEY=...AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.comGROQ_API_KEY=gsk_...MISTRAL_API_KEY=...COHERE_API_KEY=...DEEPSEEK_API_KEY=...TOGETHER_API_KEY=...FIREWORKS_API_KEY=...PERPLEXITY_API_KEY=pplx-...AI21_API_KEY=...HUGGINGFACE_API_KEY=hf_...XAI_API_KEY=...CEREBRAS_API_KEY=...SAMBANOVA_API_KEY=...REPLICATE_API_TOKEN=r8_...ELEVENLABS_API_KEY=...ASSEMBLYAI_API_KEY=...Production Security Checks
When NODE_ENV=production, the server enforces additional validation at startup:
JWT_SECRETmust not contain known placeholder stringsJWT_REFRESH_SECRETmust not contain known placeholder stringsENCRYPTION_KEYmust not use sequential hex patterns (e.g.,0123456789abcdefrepeated)REDIS_PASSWORDmust be set (non-empty)
If any check fails, the server exits with a descriptive error message.