# Postgres replaces SQLite as the primary datastore Status: accepted The project is moving from a single-reader tracker to a service published to a community, so we replaced `modernc.org/sqlite` with Postgres (`jackc/pgx/v5`, still pure Go, so `CGO_ENABLED=0` and the distroless image are unaffected). The deciding reason is future supportability — managed hosting, a datastore that survives the app outgrowing one box — **not** concurrency, which was measured and found to be a non-issue. ## Considered options **Stay on SQLite.** Benchmarked against the real store at 1,500 rows (≈50 readers × 30 series): ~9,700 upserts/sec single-writer, plateauing at ~780/sec under 8–50 concurrent writers, with `List()` holding 90–103 calls/sec under continuous write load. Projected real load at 50 readers is ~0.02 writes/sec — roughly four and a half orders of magnitude of headroom. `SetMaxOpenConns(1)` serialises writes but was shown not to starve reads; an apparent read collapse traced to row count and per-row scanning, not lock contention. SQLite would have worked. It was rejected for where the project is going, not for what it does today. **Postgres.** Chosen. Migrating is cheapest now — 29 rows in one table — and gets materially harder once there are live readers and a multi-tenant schema. ## Consequences - Every statement in `internal/store` is rewritten: `?` → `$N`, `IS NOT` → `IS DISTINCT FROM` (this one is load-bearing; it implements the `updated_at` ordering rule), `pragma_table_info` → `information_schema.columns`, `INTEGER`/`REAL` → `bigint`/`double precision`, `favorite` int-as-bool → `boolean`. - ~75 tests currently get a free isolated database from `t.TempDir()`. They now need a live server, which makes Docker a hard prerequisite for `go test ./...`. This is the permanent cost of the decision and the main reason it was close. - Backups get worse, not better: `VACUUM INTO` produced one self-contained file; restoring now means `pg_dump`/`pg_restore`, a role, and a password. - A second stateful container joins the VPS alongside the existing headless-shell. - **Postgres does not address the real scaling limit.** At batch 14 per 10-minute tick the poller checks at most 84 series/hour; 50 readers × 30 series is 1,500 bookmarks, an 18-hour sweep against a configured 1-hour cooldown. That ceiling is an outbound fetch budget and is fixed by deduplicating polls per Series, not by the datastore.