Control-plane config & secrets
Basic backend settings, required secrets, and production configuration reminders.
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.
Basic environment variables
This is a starter list. Set EXO_DEV_MODE=false in production (it defaults to true). Multiple replicas also need EXO_PEER_SERVICE and a shared EXO_INTERNAL_SECRET. Recording storage is covered under Database; public app access under Apps. For the full configuration list, see backend/config/config.go in your release source.
| 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_ENCRYPTION_KEY is required (must be a 32-byte AES-256 key). Lose it after data is written and that data can't be decrypted again. Store it in a vault and back it up separately from the database.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. The Helm chart uses supplied values first, then reuses the existing Secret, and generates missing secrets otherwise. Preserve the Secret on upgrades and back it up, especially the encryption key. 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"
openssl rand -hex 16 emits 32 hex characters, exactly 32 bytes as a string, which is what the key check wants. Avoid base64 32 here: it produces ~44 characters and fails the 32-byte check.