Skip to main content

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​

VariableRuleRequiredDefault
DATABASE_URLnon-empty Postgres connection string✅—
APP_URLURL; also decides cookie Secure + OIDC redirect rebuild✅—
INFERENCE_BASE_URLURL—http://airs-gw.airs-gw.svc.cluster.local:80
INFERENCE_MODELstring—@vllm2/unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL
INFERENCE_API_KEYstringoptionalunset — header omitted when absent; required when INFERENCE_AUTH_MODE=gateway, and if a direct vLLM runs with --api-key
INFERENCE_AUTH_MODEdirect | gateway—direct — gateway sends x-portkey-api-key; direct sends Authorization: Bearer
MCP_AUTH_MODEdirect | gateway—direct — gateway sends x-portkey-api-key + X-Auth-Token; direct sends Authorization: Bearer
KC_ISSUERURL (OIDC realm base)✅—
KC_CLIENT_IDnon-empty string✅—
KC_CLIENT_SECRETnon-empty string✅—
SESSION_SECRETstring, min length 32 (sha256'd into the AES key)✅—
ADMIN_USERNAMEnon-empty string✅—
ADMIN_PASSWORDnon-empty string✅—
M2M_SCOPEstring — the scope a bearer token must carry—clippy-api
M2M_AUDIENCEstring — the aud a bearer token must carryoptionalunset — 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_URL does double duty: cookies get the Secure flag only when it is https, and the OIDC callback rebuilds redirect_uri against it (needed behind a TLS-terminating proxy).
  • SESSION_SECRET is sha256'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_SCOPE is the primary authorization check for machine tokens. The red-team adapter must request this scope or every probe returns 401.
  • M2M_AUDIENCE adds a second, independent check: when set, verifyBearer requires aud and demands it contain this value, so a correctly scoped token minted for a different audience in the same realm no longer authenticates. Production sets stack-clippy. Leave unset only while arming is unsafe: setting it to a value the caller does not carry 401s every machine caller, and there is no warn-on-mismatch mode. Today clippy-m2m carries the bare string stack-clippy while clippy-mcp-client carries ["clippy", "stack-clippy"], so stack-clippy enforces with no realm change and clippy-api needs one audience mapper. Check a real token first: k8s/m2m-audience-rollout.md.
  • ADMIN_PASSWORD is only applied when the admin row is first seeded (see Authentication).