Skip to main content
View as Markdown

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.

>_ terminal
# .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.

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
>_ terminal
# .env — single reverse proxy in front of the app
TRUSTED_PROXY_HOPS=1

Leaving 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).
code
.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 overrides

SOVRIUM_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:

>_ terminal
sovrium secret generate            # both
sovrium secret generate auth       # AUTH_SECRET only
sovrium secret generate encryption # SOVRIUM_ENCRYPTION_KEY only

How the encryption key is resolved

  1. SOVRIUM_ENCRYPTION_KEY when set — it wins, and nothing is written to disk.
  2. <data dir>/encryption-key — the key this install generated on an earlier start.
  3. Otherwise 256 fresh bits, written to <data dir>/encryption-key at mode 0600.

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:

code
✓ Encryption key: from SOVRIUM_ENCRYPTION_KEY
✓ Encryption key: from /var/lib/sovrium/encryption-key
✓ Encryption key: generated at /var/lib/sovrium/encryption-key

Seeing 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_URL on a host whose filesystem resets on deploy or restart: Heroku, Render, Scalingo, docker run without 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:

code
⚠ 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 restart

Dropping 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.

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

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.

Built with Sovrium