Configuration
All configuration is via environment variables, parsed and validated once at startup with
zod (src/lib/env.ts). A missing or invalid required variable throws on the first env()
call — the app fails fast rather than booting half-configured.
Variables
| Variable | Rule | Required | Default |
|---|---|---|---|
DATABASE_URL | non-empty Postgres connection string | ✅ | — |
APP_URL | URL; also decides cookie Secure + OIDC redirect rebuild | ✅ | — |
INFERENCE_BASE_URL | URL | — | http://airs-gw.airs-gw.svc.cluster.local:80 |
INFERENCE_MODEL | string | — | @vllm2/unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL |
INFERENCE_API_KEY | string | optional | unset — header omitted when absent; required when INFERENCE_AUTH_MODE=gateway, and if a direct vLLM runs with --api-key |
INFERENCE_AUTH_MODE | direct | gateway | — | direct — gateway sends x-portkey-api-key; direct sends Authorization: Bearer |
MCP_AUTH_MODE | direct | gateway | — | direct — gateway sends x-portkey-api-key + X-Auth-Token; direct sends Authorization: Bearer |
KC_ISSUER | URL (OIDC realm base) | ✅ | — |
KC_CLIENT_ID | non-empty string | ✅ | — |
KC_CLIENT_SECRET | non-empty string | ✅ | — |
SESSION_SECRET | string, min length 32 (sha256'd into the AES key) | ✅ | — |
ADMIN_USERNAME | non-empty string | ✅ | — |
ADMIN_PASSWORD | non-empty string | ✅ | — |
M2M_SCOPE | string — the scope a bearer token must carry | — | clippy-api |
M2M_AUDIENCE | string — the aud a bearer token must carry | optional | unset — audience unchecked (see below) |
.env.example
.env.example
DATABASE_URL=postgres://clippy:clippy@localhost:5433/clippy
APP_URL=http://localhost:3000
# In-cluster AIRS gateway; external callers use https://airs.cdot.io
INFERENCE_BASE_URL=http://airs-gw.airs-gw.svc.cluster.local:80
INFERENCE_MODEL=@vllm2/unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL
INFERENCE_API_KEY= # gateway key (gateway mode) or vLLM --api-key (direct mode)
INFERENCE_AUTH_MODE=direct # gateway ⇒ x-portkey-api-key; direct ⇒ Authorization: Bearer
KC_ISSUER=https://auth.example.com/realms/myrealm
KC_CLIENT_ID=clippy-web
KC_CLIENT_SECRET=changeme
SESSION_SECRET=<openssl rand -base64 32>
ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme
M2M_SCOPE=clippy-api
# Expected aud on machine bearer tokens. Verify a real token carries this value
# before setting it — arming it against a missing aud 401s every machine caller.
# M2M_AUDIENCE=clippy-api
:::danger Never commit a real .env
.env is git-ignored. Only .env.example (all placeholders) is committed. Generate a real
SESSION_SECRET with openssl rand -base64 32 and set strong admin/client secrets in your
deployment's secret store — not in the repo.
:::
Notes on individual variables
APP_URLdoes double duty: cookies get theSecureflag only when it ishttps, and the OIDC callback rebuildsredirect_uriagainst it (needed behind a TLS-terminating proxy).SESSION_SECRETissha256'd to derive the AES-256-GCM key that encrypts stored provider tokens. Rotating it invalidates existing encrypted tokens — affected sessions are transparently destroyed and users re-log-in.M2M_SCOPEis the primary authorization check for machine tokens. The red-team adapter must request this scope or every probe returns401.M2M_AUDIENCEadds a second, independent check: when set,verifyBearerrequiresaudand demands it contain this value, so a correctly scoped token minted for a different audience in the same realm no longer authenticates. Production setsstack-clippy. Leave unset only while arming is unsafe: setting it to a value the caller does not carry401s every machine caller, and there is no warn-on-mismatch mode. Todayclippy-m2mcarries the bare stringstack-clippywhileclippy-mcp-clientcarries["clippy", "stack-clippy"], sostack-clippyenforces with no realm change andclippy-apineeds one audience mapper. Check a real token first:k8s/m2m-audience-rollout.md.ADMIN_PASSWORDis only applied when the admin row is first seeded (see Authentication).