Configuration & environment reference
A flat reference, not a walkthrough — see the
install guide for the
narrative. Source of truth: .env.example (the commented
template every deployment starts from) and
apps/api/app/core/config.py (the code that reads it).
General & security
| Variable | Default | Meaning |
|---|---|---|
ENVIRONMENT | development | development / staging / production. Production enforces the hardening below regardless of other settings. |
LOG_LEVEL | INFO | Application log verbosity. |
SECRET_KEY | insecure placeholder | JWT signing key. The app refuses to start in production if empty, a placeholder, or shorter than 32 chars. Generate with openssl rand -hex 32. |
DATA_ENCRYPTION_KEY | empty | Separate at-rest encryption key for connector/SMTP/webhook secrets (two-key model). Empty falls back to deriving from SECRET_KEY (backward-compatible with pre-split installs). Production requires it distinct from SECRET_KEY, ≥32 chars. |
ACCESS_TOKEN_EXPIRE_MINUTES / REFRESH_TOKEN_EXPIRE_DAYS | 30 / 14 | JWT session lifetimes. |
SESSION_COOKIE_SECURE | false | Production always forces Secure cookies regardless; set true for any HTTPS staging environment too. |
CORS_ORIGINS | http://localhost:3000 | Comma-separated allowed browser origins. The API always runs with credentials on, so * is rejected at startup — list each origin explicitly. |
RATE_LIMIT_ENABLED / RATE_LIMIT_BACKEND | true / memory | Per-IP brute-force protection. memory = per API process; redis = shared across replicas (production/staging overlays force redis). |
AUTH_RATE_LIMIT_PER_MIN / WRITE_RATE_LIMIT_PER_MIN | 10 / 240 | Per-IP throttle on auth endpoints, and on mutating (POST/PUT/PATCH/DELETE) requests. |
REGISTRATION_ENABLED | true | true = anyone can create an organization (SaaS); false = private instance, invite-only (the production overlay pins this false — the first account can still be bootstrapped). |
Reverse proxy & TLS
| Variable | Default | Meaning |
|---|---|---|
TLS_CN / TLS_SAN | cerynix.local / DNS:localhost | Subject/SANs for the proxy's auto-generated self-signed certificate on first boot; replace via Settings → TLS once you have a real one. |
PROXY_HTTP_PORT | 8080 | Host port for plain HTTP; set to 80 in production (firewall opens 22/80/443). |
NEXT_PUBLIC_API_URL | empty | Baked into the web build (build arg, not read at runtime). Leave empty for same-origin through the proxy; set a full URL only for direct dev against the API. |
VENDOR_ORG_ID | empty | SaaS-only: UUID of the vendor's own org. When set, only that org's admins may replace the shared TLS certificate. |
TLS_EXPECTED_HOSTNAMES | empty | Comma-separated hostnames an uploaded certificate must cover via SAN, or it is rejected before reaching the proxy. |
Database, cache & storage
| Variable | Default | Meaning |
|---|---|---|
DATABASE_URL | local Postgres | Async SQLAlchemy URL (postgresql+asyncpg://…). |
DB_RLS_ENABLED | false | PostgreSQL row-level security (defense-in-depth). The Hetzner production overlay turns it on and points the app at the non-superuser cerynix_app role. Requires PostgreSQL — inert on SQLite. |
APP_DB_PASSWORD | dev placeholder | Password for the non-superuser app role used when RLS is on. |
REDIS_URL | local Redis | Cache + rate-limit backend. |
STORAGE_BACKEND | local (compose default s3) | s3 (MinIO/S3) or local filesystem for evidence. |
S3_ENDPOINT, S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, S3_USE_SSL | compose defaults | MinIO/S3 connection for evidence storage. |
LOCAL_STORAGE_PATH | ./local_storage/evidence | Used only when STORAGE_BACKEND=local. |
MAX_UPLOAD_BYTES | 50 MB | Evidence upload size cap. |
MAIL_SMTP_HOST / _PORT / _USERNAME / _PASSWORD / _USE_TLS, MAIL_FROM | empty | Notification email. Configurable in-app under Settings → Email server, with "send test email". |
SEED_DEMO_DATA | false | Seeds the demo organization + control library on startup. The production overlay hard-pins this false regardless. |
Backups
| Variable | Default | Meaning |
|---|---|---|
BACKUP_AGE_RECIPIENT | empty | Space-separated age public recipient key(s). Required in production — the in-app and script backups refuse to run without it. Outside production, an unset value falls back to a built-in (SECRET_KEY-derived) key. |
BACKUP_RETENTION_DAYS | 14 | Days before an old backup is deleted. |
BACKUP_DATABASE_URL | falls back to DATABASE_URL | pg_dump connection override — required on RLS deployments, since the app's cerynix_app role runs under FORCE ROW LEVEL SECURITY, which pg_dump refuses. Point this at the bootstrap superuser instead. |
BACKUP_SCHEDULE_HOUR | 2 | UTC hour for the worker's daily backup; negative disables the schedule. |
See the full runbook: Backup & disaster recovery.
Licensing & control plane
| Variable | Default | Meaning |
|---|---|---|
LICENSE_KEY | empty | Signed license key. Empty = Evaluation edition. Prefer activating in the web UI (Settings → License); this variable is the air-gapped/fleet-automation fallback. |
CONTROL_PLANE_URL | empty | Base URL of the Cerynix Control Plane. Empty = no phone-home. Must be https:// if set (rejected at startup otherwise), since the check-in carries CHECKIN_KEY. |
CP_TRUSTED_HOSTS | empty | Comma-separated allow-list of hostnames CONTROL_PLANE_URL may resolve to — defense-in-depth against a tampered .env redirecting the heartbeat. |
AUDIT_SIGNING_KEY_PATH / AUDIT_SIGNING_PUBKEY_PATH | empty | Per-deployment minisign (Ed25519) keypair for signing Audit Room export packages. install.sh generates these for a real deployment; empty in dev makes package export fail closed with a clear error, rather than exporting unsigned. |
Installer CLI flags
./install.sh [flags] (or passed through the one-line bootstrap after bash -s --):
| Flag | Env var | Meaning |
|---|---|---|
--hostname | CERYNIX_HOSTNAME | Hostname/IP clients use (drives TLS SAN, CORS, API URL). |
--admin-email | CERYNIX_ADMIN_EMAIL | First admin login email. |
--org | CERYNIX_ORG | Organization name. |
--country | CERYNIX_COUNTRY | Organization country, ISO code or EU (default). |
--admin-password | CERYNIX_ADMIN_PASSWORD | Avoid this — leaks into shell history and process list. Omit it (recommended); the installer prints a strong auto-generated password once. |
--admin-password-file | CERYNIX_ADMIN_PASSWORD_FILE | Read the admin password from a file — the non-leaking automation option. |
--no-backup-key | — | Skip creating the age backup-encryption key. |
--force | — | Regenerate .env even if one exists (rotates all secrets). |
--repair | — | Rebuild + restart + self-test an existing install, without rotating secrets or touching the admin account. |
--update | — | Update to the latest signed release in place. |
--allow-update-without-db-backup | — | Skip only the pre-upgrade database backup during an update — never disables signature verification, the application snapshot, or the self-test. Leaves no database recovery path; use only with an independently verified external backup already in hand. |
-y, --yes | — | Never prompt; fail if a required value is missing (for automation). |
Full narrative: install guide.
Operational scripts
| Script | Purpose |
|---|---|
scripts/backup.sh | Encrypted (age) database + evidence backup to backups/<timestamp>/. |
scripts/restore.sh | Restore a backup using the off-box age private identity. |
scripts/restore-drill.sh | Restores the latest backup into a throwaway, isolated Postgres container, runs smoke checks, and reports measured RTO/RPO. |
scripts/backup-r2.sh | Example off-box copy to Cloudflare R2 (any off-box object store works the same way). |
scripts/seed_demo_data.py | Idempotent demo organization + control library seed (also runnable as python -m app.services.seed.seeder from apps/api). |
Full procedures: Backup & disaster recovery.
API reference
The interactive API reference — Swagger at /docs
and ReDoc at /redoc, plus the schema at
/api/v1/openapi.json — is always available in non-production
environments. In production all three are off by default, including on a
quick-installed on-prem box: they are unauthenticated, so publishing them would hand
anyone who can reach the host a map of the API. They will 404 unless you opt in.
You do not need them to read the spec. The in-console API Reference page
(Platform → API Reference) renders the live document for the running build behind
your normal login and offers a "Download openapi.json" button, and
docs/openapi.json ships with every release. If you do want
Swagger/ReDoc on an instance you know is internal-only, set
API_DOCS_ENABLED=true in .env and
run ./install.sh --repair.