Control-plane config & secrets
Every environment variable the Exo backend reads, which ones are mandatory, and how to generate the two secrets that gate startup.
The backend is configured through environment variables: typically a ConfigMap for the non-secret ones and a Secret for the rest. Two are mandatory and the process fails fast without them. There are no production-safe defaults for a signing key or an encryption key.
Environment variables
| Variable | Required | Default | Purpose | Example |
|---|---|---|---|---|
EXO_ENCRYPTION_KEY | ✓ yes | (none) | AES-256 root key, exactly 32 bytes | 9f2a4c6e8b0d1f3a5c7e9b1d3f5a7c9e |
EXO_JWT_SECRET | ✓ yes | required — no default | HS256 key for user + OAuth tokens | xK9wL2mN… |
EXO_DATABASE_DSN | ✓ prod | localhost dev DSN | Postgres connection string | postgres://exo:…@db.acme.internal:5432/exo |
EXO_PORT | no | 9092 | HTTP listen port | 9092 |
EXO_BASE_URL | no | http://localhost:9092 | Public URL (email links, OAuth cb) | https://exo.acme.com |
EXO_JWT_EXPIRY | no | 24h | User JWT lifetime (Go duration) | 24h |
EXO_CAST_STORAGE_DIR | no | /var/lib/exo/casts | Session-recording dir (writable vol) | /var/lib/exo/casts |
EXO_CLAUDE_API_KEY | no | (empty) | base64 Anthropic key, AI fallback | c2stYW50LWFwaTAzL… |
EXO_ENCRYPTION_KEY
Root of the envelope-encryption keyring. It wraps the key-encryption key, which in turn encrypts every sensitive column at rest: integration credentials, identity-provider secrets, log-sink configs, tool permissions, session payloads. A database dump is useless without it.
EXO_JWT_SECRET
Signs every user JWT and every short-lived OAuth access token (HS256). Rotating it invalidates all active sessions and issued tokens immediately, so treat it like a database password: vault it, rotate on a schedule, never commit it. There is no default: EXO_JWT_SECRET is required, and the well-known value changeme-secret-key is rejected at boot so a forgeable instance can't start.
EXO_CLAUDE_API_KEY
Optional. A platform-default Anthropic key used by the "Generate with AI" features (skill generation) when a tenant hasn't configured its own provider. It must be base64-encoded in the Secret; the backend decodes it at read time. Leave it blank to disable the fallback. In production, prefer Claude Workload Identity Federation over a static key.
printf '%s' 'sk-ant-...' | base64
Generating secrets
If you use exo-install, the JWT secret, encryption key, and (with the bundled Postgres) the database password are generated for you. With the Helm chart you create the key Secret yourself and the chart reads it by reference (it won't install otherwise). For manual installs:
EXO_ENCRYPTION_KEY="$(openssl rand -hex 16)"
EXO_JWT_SECRET="$(openssl rand -base64 48)"
kubectl -n exo create secret generic exo-secrets \--from-literal=EXO_ENCRYPTION_KEY="$EXO_ENCRYPTION_KEY" \--from-literal=EXO_JWT_SECRET="$EXO_JWT_SECRET" \--from-literal=EXO_DATABASE_DSN="host=db user=exo dbname=exo sslmode=require"