# ADR-001: SQLite, not PostgreSQL — with a seam for later ## Status Accepted ## Date 2026-10-12 ## Context Vellum stores everything in one database: users, grants, the catalogue, per-user progress, the job queue and API tokens. The choice of engine determines the deployment story, the backup story, and how much concurrent writing is possible. The original motivation for considering PostgreSQL was concurrency: SQLite allows only one writer at a time, and a library scan holding the write lock would block a playing client's progress save. That problem turned out to be solvable without changing engines, and it was measured rather than assumed. ## Decision Use SQLite with WAL, and put a storage seam behind the one package that reads items, so the engine is swappable later if a real need appears. ## Alternatives Considered ### PostgreSQL - **Pros:** real concurrent writers; no single-writer bottleneck; mature. - **Cons:** a second process to run and back up; the deployment story stops being "one binary and one file"; the SQL is not portable — 186 statements, 193 `?` placeholders, FTS5 full-text search, `strftime`, `group_concat`, `rowid`, and `VACUUM INTO` for backups all need reworking. - **Rejected for now.** The problem it solves was already fixed by other means (see below), and there is no measured write contention left. ### Fixing the write-lock problem in code instead Three changes removed the symptom without changing engines: 1. **Chunked scan transactions.** 500 groups per transaction instead of one transaction over the whole library. Measured: a 20,000-file scan reports partial progress and a concurrent progress save returns 200 throughout. 2. **Four job workers** with jittered idle waits, so a scan no longer stalls every other job. 3. **Throttled session writes.** `last_seen` was written on *every* authenticated request, including every audio Range request. Measured after the fix: 20 requests cause **0** session writes, down from 20. Measured results after these changes: browse page 1 at **0.03 ms**, the home screen at ~3 ms, and two concurrent 8,000-file scans interleaving successfully with every concurrent write returning 200. ## Consequences - Deployment stays one binary plus one file. Backup is `VACUUM INTO`. - Writes serialise. This is fine at the measured scale and is the honest ceiling of this design. - `internal/catalog` exposes a `Store` interface with a SQLite implementation, so a PostgreSQL implementation would be a second `Store` rather than a rewrite. - The seam is guarded: `internal/arch` fails the build if a new package starts importing `database/sql` without being declared, or if a package already on the seam reaches for `*sql.DB` again. - **When to revisit:** multiple Vellum instances sharing a database, or write throughput beyond what chunked WAL handles. Neither is true today.