Database Infrastructure
Sovrium runs on an embedded SQLite database with zero configuration and scales up to PostgreSQL by setting a single environment variable. The entire runtime — migrations, client bundles, the CSS engine, example configs — is bundled into one standalone binary, and a static page-render cache keeps server CPU frugal. This page documents the persistence and distribution layer that makes "run the binary, open the URL" possible.
Zero-Config SQLite Default
When no DATABASE_URL is set, Sovrium resolves the sqlite dialect, creates ./.sovrium/database.db, auto-applies the SQLite migration set on first boot, and serves the full core surface — schema, auth, records CRUD, migrations, and forms. The startup banner prints Database: SQLite (<absolute path>).
This mirrors the platform's frugal-by-default philosophy (twin of STORAGE_PROVIDER and the ECO_* family): the operator opts into PostgreSQL by setting DATABASE_URL, never the other way around.
PostgreSQL When Configured
Set DATABASE_URL to a postgresql:// URL and Sovrium resolves the postgres dialect, connects, applies the PostgreSQL migration set, and prints Database: PostgreSQL. No SQLite file is created. This path is byte-identical to pre-existing PostgreSQL deployments.
A single source of truth — parseDatabaseDialectConfig() — scheme-discriminates the one DATABASE_URL variable:
DATABASE_URL value |
Resolves to |
|---|---|
postgres://… / postgresql://… |
PostgreSQL at that URL. |
| unset / empty | SQLite at ./.sovrium/database.db (the zero-config default). |
file:./x.db / file:/abs.db |
SQLite at the resolved path. |
sqlite:./x.db / sqlite:///abs.db |
SQLite alias for the same on-disk file. |
:memory: |
Ephemeral SQLite (dialect-level only — not a full deployment target). |
anything else (bare path, mysql://, …) |
Throws Unsupported DATABASE_URL scheme: … at boot (fail-loud). |
SQLITE_PATH was removed (pre-1.0 breaking change, no compatibility bridge). The SQLite file location is now chosen exclusively through DATABASE_URL via the file: / sqlite: schemes. A bare filesystem path with no scheme (DATABASE_URL=./database.db) is rejected at boot — prefix it with file:.
The selected dialect drives the Drizzle client (bun:sqlite or bun:sql), the migration set, the Better Auth adapter provider, and the operator-facing runtime label from GET /api/admin/config/version (sqlite-aio on SQLite, postgres on PostgreSQL).
Graceful degradation
SQLite mode supports the core subset. Advanced PostgreSQL-only features — notably pgvector RAG semantic search — return a typed 501 { error: 'requires-postgres' } rather than crash. Scale up to PostgreSQL to unlock them; no code changes required.
Embedded Data Directory
All local state lives under a single, relocatable data directory.
| Env var | Default | Description |
|---|---|---|
DATABASE_URL |
unset → SQLite | Database engine + location selector. |
SOVRIUM_DATA_DIR |
./.sovrium |
Root for the SQLite DB file, lock files, and local storage. Relocate the whole data dir with one variable. |
The default ./.sovrium/database.db sits under this data dir; the parent directory is created automatically on first boot.
Static Page Render Cache
Sovrium renders pages server-side on every request. For a page whose HTML does not depend on per-request state (a marketing, about, or pricing page), that is wasted CPU. The in-memory render cache renders such a page once and serves it from memory on subsequent anonymous requests, skipping renderToString entirely — an ecoconception and latency win.
| Env var | Default | Options | Description |
|---|---|---|---|
ECO_PAGE_CACHE |
on |
on | off |
In-memory cache of page HTML. Operators opt out; they never opt in. |
ECO_PAGE_CACHE_MAX_MB |
64 |
integer (MB) | Memory budget for the cache before the oldest entries are evicted. |
How it works:
- Cacheability is derived, never authored — as a three-way verdict. A page is
staticwhen its rendered HTML cannot vary by request. A page iscontentwhen its only out-of-schema input is a directory of markdown files: it owns acontentDirand trips nothing beyondcontentDir,markdown, and its:paramroute. Everything else isdynamicand never cached — a page with non-publicaccess, acollection, a page/componentdataSource, a filesource,presence, a data-bound sidebar, or a:paramroute with nocontentDirbehind it (there is no corpus to measure, so nothing can prove its render invariant). - Safety model. The cache is consulted only for anonymous, non-preview requests; session-dependent filtering is deterministic when no session exists, so one user's view can never leak to another.
- Invalidation by checksum. Each entry is keyed by
${appRenderChecksum}:${path}:${language}. The checksum is a SHA-256 of the render-affecting schema slice (pages,components,theme,languages,analytics); any schema edit changes every key, so stale entries become unreachable. No TTL, no purge logic. - Content pages are keyed by their corpus too. A
contentpage extends that key with a checksum of itscontentDir: the sorted(path, mtimeMs, size)tuples of every markdown file, re-scanned per request (a glob plus onestateach — roughly 1-2 ms for a 200-article corpus). Editing, adding, or removing an article changes the checksum, so the next request misses and serves the new prose. BothmtimeMsandsizeparticipate, so a same-millisecond rewrite of a different length is still caught. No watcher, no restart, no purge endpoint. - Bounded by bytes, not by entry count.
ECO_PAGE_CACHE_MAX_MB(default64) caps the total HTML held; past the budget, insertion-order-oldest entries are evicted until the new entry fits, and an entry larger than the whole budget is refused. Bytes are the right unit once whole docs zones are cacheable: 200 marketing pages and 200 long-form articles differ by an order of magnitude. - Observability. Every page response carries
X-Render-Cache: hit | miss | bypassand aCache-Controlheader:public, max-age=300for cacheable pages,public, max-age=60, stale-while-revalidate=300for anonymous renders of non-cacheable paths (browsers and CDNs may share them briefly), andprivate, no-cachefor authenticated or preview renders. Language variants (/vs/fr/) are isolated by key.
See Ecoconception for the full ECO_* env-var contract.
Standalone Binary Distribution
Sovrium's primary distribution channel is a standalone binary (bun build --compile) that runs with no Bun or Node.js on the target host. The binary embeds the entire runtime into a single executable:
- CLI command surface (
--version,validate,build,schema,init) - Migration sets (both dialects)
- Client bundles and island chunks
- Init templates (each carrying its own Claude Code bundle) and example configs
- The native-free themed CSS engine
In compiled mode the binary reads its embedded assets, never the repo's on-disk dist//src//specs/ trees — so it behaves identically when launched from any working directory on a customer machine. Embedded client bundles, island chunks, and the language-switcher script are served over HTTP from a binary-launched server (GET /assets/output.css, GET /assets/client.js, GET /assets/islands/*).
Related Pages
- Installation — getting the binary and running it.
- Environment Variables — full env-var reference incl.
DATABASE_URL,SOVRIUM_DATA_DIR. - Schema Migrations — the migration sets each dialect applies.
- Buckets Overview — local vs S3 storage, the
STORAGE_PROVIDERtwin. - Ecoconception — the
ECO_PAGE_CACHEand broader frugal-by-default contract. - Admin Dashboard —
config/versionreports the active runtime.
Last updated August 11, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.