Environment Variables: App, Server & Database
Sovrium reads infrastructure configuration from environment variables, never from the app schema. The schema describes your application; the environment describes the machine it runs on. Set variables in a .env file beside your config, or in your host's environment.
Everything is optional. sovrium start app.yaml boots zero-config with embedded SQLite, local file storage, and an encryption key the app generates for itself on first start. The one variable worth a decision before you deploy is SOVRIUM_ENCRYPTION_KEY — see Secrets.
Storage, AI, email, MCP, eco, and observability variables are covered on Environment Variables: Services.
# .env
PORT=3000
BASE_URL=https://myapp.example.com
NODE_ENV=production
TRUSTED_PROXY_HOPS=1
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
SOVRIUM_ENCRYPTION_KEY=<64 hex characters>That is a complete deployed configuration — one secret, not two. AUTH_SECRET is optional and derives from the encryption key; set it only when you want to pin it separately.
Application and server
| Variable | Default | Description |
|---|---|---|
APP_SCHEMA |
— | App schema as inline JSON, inline YAML, or a remote URL. Alternative to passing a file path to sovrium start. |
PORT |
3000 |
Port to bind (1–65535). |
HOSTNAME |
localhost |
Network interface to bind. |
BASE_URL |
http://localhost:PORT |
Canonical public origin. Used for authentication callbacks, email links, and OAuth issuer URLs. |
NODE_ENV |
unset | Runtime environment. Set to production on every deployed instance — see below. |
TRUSTED_PROXY_HOPS |
0 |
Number of reverse proxies in front of the app (0–10). Set it whenever anything sits between the internet and Sovrium — see below. |
Why NODE_ENV=production matters
NODE_ENV=production is what turns on immutable caching for content-hashed assets. With it, hashed island chunks are served Cache-Control: public, max-age=31536000, immutable and other static assets get a one-hour cache. Without it, every asset is returned no-store, no-cache, must-revalidate and the browser refetches the whole bundle on every page view — roughly a twentyfold increase in requests per page for identical bytes.
Transport security does not depend on NODE_ENV. Secure cookies and CSRF enforcement are decided by the bind posture: a non-loopback BASE_URL or HOSTNAME forces them on, a loopback bind relaxes them so http://localhost works in development. Set a real BASE_URL in production and the secure posture follows.
Running behind a reverse proxy
Rate limits, spam guards, and abuse counters need to know which client a request came from. When Sovrium is bound directly to a port, that is simply whoever opened the connection. Behind a proxy, every request arrives from the proxy instead, and the real client address is carried in a forwarding header.
A client can also send that header itself. Proxies append to X-Forwarded-For rather than replacing it, so anything the client supplied stays at the front of the list and only the entries at the end were written by infrastructure you control. TRUSTED_PROXY_HOPS tells Sovrium how many entries at the end to believe. Until it is set, no forwarding header is believed at all.
| Deployment | Value |
|---|---|
sovrium start bound straight to a port |
0 |
| Behind one proxy — Caddy, nginx, or a PaaS router | 1 |
| Cloudflare in front of your own proxy | 2 |
# .env — single reverse proxy in front of the app
TRUSTED_PROXY_HOPS=1Leaving it unset on a proxied deployment is safe but blunt: every visitor resolves to the proxy's own address, so they share one rate-limit budget and one visitor's burst can throttle everybody. The first request that arrives with a forwarding header logs a one-time warning naming this variable.
Setting it higher than the number of proxies you actually run is the case to avoid. The count reaches that many entries back from the end of the chain, so an inflated value reaches into entries the client wrote — which lets a caller choose their own rate-limit bucket and slip the limits entirely. Count only the proxies you operate.
Data directory
Runtime artefacts live under a single directory so a fresh project root stays clean.
| Variable | Default | Description |
|---|---|---|
SOVRIUM_DATA_DIR |
./.sovrium |
Base directory for runtime-generated artefacts. Resolved to an absolute path. |
SOVRIUM_LOCK_DIR |
data dir | Directory holding the server lock file (PID plus config hash). |
.sovrium/
database.db # SQLite default — DATABASE_URL overrides
encryption-key # per-install root secret — SOVRIUM_ENCRYPTION_KEY overrides
lock # server PID + config hash — SOVRIUM_LOCK_DIR overrides
storage/ # local file uploads — STORAGE_LOCAL_DIRECTORY overridesSOVRIUM_DATA_DIR only moves the fallback location. Each artefact keeps its own dedicated override, and that override always wins.
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
unset (SQLite) | Connection string. The scheme selects the engine — see below. |
DATABASE_POOL_MAX |
10 |
PostgreSQL connection-pool size. Ignored by SQLite. |
DATABASE_URL is scheme-discriminated, and Sovrium fails loudly at startup on anything it does not recognise:
| Value | Engine |
|---|---|
| unset or empty | SQLite at <data dir>/database.db — the zero-config default |
postgresql://user:pass@host:5432/db |
PostgreSQL (postgres:// is equally accepted) |
file:./data/app.db, sqlite:./app.db, :memory: |
SQLite at that path (:memory: is ephemeral) |
A bare filesystem path is rejected — prefix it with file:. See Database Infrastructure for the dialect differences.
Upgrading timestamp columns (PostgreSQL only)
A created-at, updated-at or deleted-at field you write out explicitly now creates a timestamptz column — the same type Sovrium has always used for the equivalent columns it adds for you. Columns created by an older version are still timestamp without a zone, and Sovrium will not silently rewrite them.
| Variable | Default | Description |
|---|---|---|
DATABASE_TIMESTAMPTZ_MIGRATION |
off | Set to on to convert those columns to timestamptz on the next start. |
DATABASE_TIMESTAMPTZ_MIGRATION_ACK_NON_UTC |
off | Set to 1 to allow the conversion when the database's time zone is not UTC. |
While the migration is off, every start logs one warning per affected column and changes nothing — existing apps upgrade exactly as before. Both column types serialise identically through the API, so leaving it off is safe indefinitely.
Turning it on rewrites each affected table and holds an exclusive lock for the duration, so treat it as a maintenance window on a large table. The conversion keeps every stored instant unchanged, provided your database's time zone has been the same for the whole life of the data.
If that time zone is not UTC, the start aborts with an error naming the zone. Two things it cannot check for itself become possible there: a time zone that was changed at some point in the past (rows written on either side of the change mean different instants, and which is which is no longer recoverable), and the repeated hour when daylight saving ends (a value in that hour is ambiguous, and up to one hour of rows per year may land an hour off). Set DATABASE_TIMESTAMPTZ_MIGRATION_ACK_NON_UTC=1 to proceed anyway, or pin the database to UTC first.
Secrets
Two secrets protect a Sovrium app: an encryption key for stored credentials, and a signing secret for sessions. Neither has to be set — the app provisions both on its own — but where the app runs decides whether that is enough.
| Variable | Default | Description |
|---|---|---|
SOVRIUM_ENCRYPTION_KEY |
generated into <data dir>/encryption-key |
Master key for encrypting stored credentials at rest (AES-256-GCM), and the root the auth secret derives from. |
AUTH_SECRET |
derived from the encryption key | Signs session cookies, tokens, and signed URLs. Minimum 16 characters when you set it; the generated value is 64 hex. |
To set them yourself, sovrium secret generate prints paste-ready .env lines to stdout and never writes them to disk:
sovrium secret generate # both
sovrium secret generate auth # AUTH_SECRET only
sovrium secret generate encryption # SOVRIUM_ENCRYPTION_KEY onlyHow the encryption key is resolved
SOVRIUM_ENCRYPTION_KEYwhen set — it wins, and nothing is written to disk.<data dir>/encryption-key— the key this install generated on an earlier start.- Otherwise 256 fresh bits, written to
<data dir>/encryption-keyat mode0600.
If the data directory cannot be written, the app refuses to start rather than run on a key it will forget. Every start reports which of the three it used:
✓ Encryption key: from SOVRIUM_ENCRYPTION_KEY
✓ Encryption key: from /var/lib/sovrium/encryption-key
✓ Encryption key: generated at /var/lib/sovrium/encryption-keySeeing generated at on a restart — rather than from — means the previous key is gone and everything encrypted under it is now unreadable.
When to set it yourself
The generated key sits beside the SQLite database it protects, so on a persistent disk the two live and die together and nothing is ever left stranded. Local development and a single-server SQLite deployment need no key at all.
Set SOVRIUM_ENCRYPTION_KEY explicitly — or let the platform's secret generator supply it — whenever the key and the data it protects do not share a fate:
- an external
DATABASE_URLon a host whose filesystem resets on deploy or restart: Heroku, Render, Scalingo,docker runwithout a volume; - any deployment where the data directory is not a persistent volume.
There the database outlives the key that encrypted it, so every stored connection token becomes unreadable on the next restart — and again on the one after that. Sovrium says so at boot when it recognises the shape:
⚠ Encryption key was generated on this boot while DATABASE_URL points at an external database
— set SOVRIUM_ENCRYPTION_KEY to a fixed value so stored connection tokens survive a restartDropping the variable on an existing install
An install that already supplies SOVRIUM_ENCRYPTION_KEY cannot simply unset it: the next start would find no key file, generate one, and orphan everything the old key encrypted. Run sovrium secret adopt first — it writes the key the process already has into the file the server reads, so removing the variable changes nothing.
Back the key up with the database. A stored credential encrypted under a key you no longer have cannot be recovered — the affected users have to reconnect. Keep <data dir>/encryption-key in your backups, or keep the value in your platform's secret store. Setting or rotating AUTH_SECRET signs every active session out.
Default admin user
Seeds an administrator on first startup. Both the email and the password must be set for seeding to happen.
| Variable | Default | Description |
|---|---|---|
AUTH_ADMIN_EMAIL |
— | Email address of the seeded administrator. |
AUTH_ADMIN_PASSWORD |
— | Password for that account. Minimum 8 characters. |
AUTH_ADMIN_NAME |
Admin |
Display name. |
AUTH_ADMIN_ROLE |
admin |
Role assigned to the seeded account. |
sovrium admin create <email> does the same job interactively, without putting a password in the environment.
OAuth providers
Each provider configured in the auth schema reads a credential pair. Replace {PROVIDER} with the uppercase provider name — GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and so on. Google, GitHub, Microsoft, Slack, GitLab, and Facebook are supported. Callback URLs are derived from BASE_URL, so it must be correct before OAuth will work.
Next
- Environment Variables: Services — storage, AI, email, MCP, eco, observability.
- Configuration Files — what belongs in the schema instead.
- Auth Overview — how
AUTH_SECRETand OAuth credentials are used. - Security Hardening — the deployment security baseline.
Last updated August 28, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.