Database · Postgres
Exo's only stateful dependency. What version, how the schema is created and maintained, and the one extra volume the backend needs beyond Postgres.
Durable control-plane state — users, tokens, audit events, deployments, runtime statuses — lives in PostgreSQL. The backend owns its schema end to end: it migrates, partitions, and bootstraps the encryption keyring at boot, so there's no separate migration job to run or sequence.
Version & DSN
Use PostgreSQL 16. The connection string is supplied via EXO_DATABASE_DSN in either libpq keyword form or a URL:
1# keyword form2host=db.example.com user=exo password=… dbname=exo port=5432 sslmode=require3 4# URL form5postgres://exo:…@db.example.com:5432/exo?sslmode=requireThe backend keeps a pool of up to 100 open / 10 idle connections.
Bundled vs managed
- Bundled (trials).
exo-installcan deploy an in-clusterpostgres:16-alpineStatefulSet and wire the DSN for you. The Docker Compose stack does the same locally. Fine for evaluation, not for production durability. - Managed (production). Point at RDS / Cloud SQL / a managed Postgres with
--postgres=false --db-dsn "…". You get backups, failover, and PITR from your provider.
Migrations at boot
On startup the backend runs, in order:
- Partition-parent creation (tables declared
PARTITION BY). - Encrypted-column migration (e.g. tool-status columns jsonb → text).
- GORM auto-migrate for all models.
- Post-migrate SQL constraints (case-insensitive email uniqueness, cascade FKs).
- Encryption-keyring bootstrap — requires
EXO_ENCRYPTION_KEY. - Monthly child-partition creation + dropping expired partitions, then a daily maintenance daemon for month rollovers and retention.
Partitioning & retention
High-volume tables (session events, audit) are time-partitioned by month. The maintenance daemon rolls new partitions forward and drops ones past the retention window. Retention is configurable per deployment from the dashboard (or exo deployment retention <id> --days N); the daemon enforces it without manual SQL.
Session-recording storage
Asciicast session recordings are written to the filesystem, not Postgres, under EXO_CAST_STORAGE_DIR (default /var/lib/exo/casts, up to 64 MiB per recording). In Kubernetes this path needs a writable volume — mount a PersistentVolume if you want recordings to survive pod restarts.
1 volumeMounts:2 - name: casts3 mountPath: /var/lib/exo/casts4 volumes:5 - name: casts6 persistentVolumeClaim:7 claimName: exo-castsBackups
Back up Postgres with your provider's tooling (or pg_dump for the bundled instance). Critically, back up EXO_ENCRYPTION_KEY separately — a database backup is unreadable without it. See Config & secrets.