Reference

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

VariableDefaultMeaning
ENVIRONMENTdevelopmentdevelopment / staging / production. Production enforces the hardening below regardless of other settings.
LOG_LEVELINFOApplication log verbosity.
SECRET_KEYinsecure placeholderJWT 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_KEYemptySeparate 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_DAYS30 / 14JWT session lifetimes.
SESSION_COOKIE_SECUREfalseProduction always forces Secure cookies regardless; set true for any HTTPS staging environment too.
CORS_ORIGINShttp://localhost:3000Comma-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_BACKENDtrue / memoryPer-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_MIN10 / 240Per-IP throttle on auth endpoints, and on mutating (POST/PUT/PATCH/DELETE) requests.
REGISTRATION_ENABLEDtruetrue = 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

VariableDefaultMeaning
TLS_CN / TLS_SANcerynix.local / DNS:localhostSubject/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_PORT8080Host port for plain HTTP; set to 80 in production (firewall opens 22/80/443).
NEXT_PUBLIC_API_URLemptyBaked 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_IDemptySaaS-only: UUID of the vendor's own org. When set, only that org's admins may replace the shared TLS certificate.
TLS_EXPECTED_HOSTNAMESemptyComma-separated hostnames an uploaded certificate must cover via SAN, or it is rejected before reaching the proxy.

Database, cache & storage

VariableDefaultMeaning
DATABASE_URLlocal PostgresAsync SQLAlchemy URL (postgresql+asyncpg://…).
DB_RLS_ENABLEDfalsePostgreSQL 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_PASSWORDdev placeholderPassword for the non-superuser app role used when RLS is on.
REDIS_URLlocal RedisCache + rate-limit backend.
STORAGE_BACKENDlocal (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_SSLcompose defaultsMinIO/S3 connection for evidence storage.
LOCAL_STORAGE_PATH./local_storage/evidenceUsed only when STORAGE_BACKEND=local.
MAX_UPLOAD_BYTES50 MBEvidence upload size cap.
MAIL_SMTP_HOST / _PORT / _USERNAME / _PASSWORD / _USE_TLS, MAIL_FROMemptyNotification email. Configurable in-app under Settings → Email server, with "send test email".
SEED_DEMO_DATAfalseSeeds the demo organization + control library on startup. The production overlay hard-pins this false regardless.

Backups

VariableDefaultMeaning
BACKUP_AGE_RECIPIENTemptySpace-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_DAYS14Days before an old backup is deleted.
BACKUP_DATABASE_URLfalls back to DATABASE_URLpg_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_HOUR2UTC hour for the worker's daily backup; negative disables the schedule.

See the full runbook: Backup & disaster recovery.

Licensing & control plane

VariableDefaultMeaning
LICENSE_KEYemptySigned license key. Empty = Evaluation edition. Prefer activating in the web UI (Settings → License); this variable is the air-gapped/fleet-automation fallback.
CONTROL_PLANE_URLemptyBase 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_HOSTSemptyComma-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_PATHemptyPer-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 --):

FlagEnv varMeaning
--hostnameCERYNIX_HOSTNAMEHostname/IP clients use (drives TLS SAN, CORS, API URL).
--admin-emailCERYNIX_ADMIN_EMAILFirst admin login email.
--orgCERYNIX_ORGOrganization name.
--countryCERYNIX_COUNTRYOrganization country, ISO code or EU (default).
--admin-passwordCERYNIX_ADMIN_PASSWORDAvoid this — leaks into shell history and process list. Omit it (recommended); the installer prints a strong auto-generated password once.
--admin-password-fileCERYNIX_ADMIN_PASSWORD_FILERead the admin password from a file — the non-leaking automation option.
--no-backup-keySkip creating the age backup-encryption key.
--forceRegenerate .env even if one exists (rotates all secrets).
--repairRebuild + restart + self-test an existing install, without rotating secrets or touching the admin account.
--updateUpdate to the latest signed release in place.
--allow-update-without-db-backupSkip 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, --yesNever prompt; fail if a required value is missing (for automation).

Full narrative: install guide.

Operational scripts

ScriptPurpose
scripts/backup.shEncrypted (age) database + evidence backup to backups/<timestamp>/.
scripts/restore.shRestore a backup using the off-box age private identity.
scripts/restore-drill.shRestores the latest backup into a throwaway, isolated Postgres container, runs smoke checks, and reports measured RTO/RPO.
scripts/backup-r2.shExample off-box copy to Cloudflare R2 (any off-box object store works the same way).
scripts/seed_demo_data.pyIdempotent 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.