Skip to main content
View as Markdown

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

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 static when its rendered HTML cannot vary by request. A page is content when its only out-of-schema input is a directory of markdown files: it owns a contentDir and trips nothing beyond contentDir, markdown, and its :param route. Everything else is dynamic and never cached — a page with non-public access, a collection, a page/component dataSource, a file source, presence, a data-bound sidebar, or a :param route with no contentDir behind 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 content page extends that key with a checksum of its contentDir: the sorted (path, mtimeMs, size) tuples of every markdown file, re-scanned per request (a glob plus one stat each — 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. Both mtimeMs and size participate, 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 (default 64) 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 | bypass and a Cache-Control header: public, max-age=300 for cacheable pages, public, max-age=60, stale-while-revalidate=300 for anonymous renders of non-cacheable paths (browsers and CDNs may share them briefly), and private, no-cache for 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/*).

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.

Built with Sovrium