Summary
Add opt-in PostgreSQL support to the cq server. SQLite remains the
zero-config default for single-instance use; PostgreSQL is opt-in for
multi-process / Kubernetes deployments. Selection via CQ_DATABASE_URL
with fallback to CQ_DB_PATH.
Server only. Go SDK, Python SDK LocalStore, and CLI local store
remain SQLite — those are offline (local) stores, not shared databases.
This epic tracks the implementation agreed in RFC #275.
Original request
Preserved from the issue history above (opened by @nissessenap,
2026-04-09): hosting cq in Kubernetes with a single SQLite instance is
not viable — backup pain, single-writer ceiling, no path to horizontal
scale.
Architecture (from RFC #275)
Store protocol (async) with one concrete implementation per backend.
SqliteStore — owns sqlite-vec, FTS5, PRAGMAs, anything SQLite-native.
PostgresStore — owns pgvector, tsvector + GIN, advisory locks,
anything Postgres-native.
- Portable queries live in a shared SQLAlchemy Core helper consumed by
both concrete stores. Strategy pattern at the protocol boundary; no
runtime if dialect == ... branches inside methods.
- Migrations: Alembic with
render_as_batch=True. Single migration
files run on both backends.
pg_advisory_lock wraps migration on Postgres to prevent multi-pod
startup race.
- Baseline migration reverse-engineered from current production schema,
stamped on first startup of existing databases.
- Timestamps cleaned up to proper
datetime at store boundary
(TIMESTAMP WITH TIME ZONE on Postgres, ISO strings on SQLite).
Phased plan
Phase 1 — make the change easy (SQLite-only, no behavior change):
extension point + migration framework. Mergeable independently.
Phase 2 — make the easy change: drop in PostgresStore, CI matrix.
Phase 3 — timestamp cleanup: proper datetime at store boundary.
Phase 4 — documentation.
Phase 1 dependency graph
Level 0: (1) deps + Alembic skeleton
│
├──────────────┬───────────────┐
▼ ▼ ▼
Level 1: (2) baseline (3) async (4) Core query
+ stamp Store helpers
protocol
│ │ │
│ └───────┬───────┘
│ ▼
Level 2: │ (5) SqliteStore
│ via protocol
│ │
│ ▼
Level 3: │ (6) CQ_DATABASE_URL
│ + factory
│
▼
Level 4: (7) remove ad-hoc _ensure_* DDL
(gate on (2) deployed)
After (1) lands, (2), (3), and (4) can proceed in parallel. (5) waits
on (3) + (4). (6) waits on (5). (7) waits on (2) being deployed
everywhere.
Child issues — Phase 1: protocol + Alembic, SQLite-only
Child issues — Phase 2: PostgresStore
Child issues — Phase 3: timestamp cleanup
Child issues — Phase 4: documentation
Summary
Add opt-in PostgreSQL support to the cq server. SQLite remains the
zero-config default for single-instance use; PostgreSQL is opt-in for
multi-process / Kubernetes deployments. Selection via
CQ_DATABASE_URLwith fallback to
CQ_DB_PATH.Server only. Go SDK, Python SDK
LocalStore, and CLI local storeremain SQLite — those are offline (local) stores, not shared databases.
This epic tracks the implementation agreed in RFC #275.
Original request
Preserved from the issue history above (opened by @nissessenap,
2026-04-09): hosting cq in Kubernetes with a single SQLite instance is
not viable — backup pain, single-writer ceiling, no path to horizontal
scale.
Architecture (from RFC #275)
Storeprotocol (async) with one concrete implementation per backend.SqliteStore— owns sqlite-vec, FTS5, PRAGMAs, anything SQLite-native.PostgresStore— owns pgvector,tsvector+ GIN, advisory locks,anything Postgres-native.
both concrete stores. Strategy pattern at the protocol boundary; no
runtime
if dialect == ...branches inside methods.render_as_batch=True. Single migrationfiles run on both backends.
pg_advisory_lockwraps migration on Postgres to prevent multi-podstartup race.
stamped on first startup of existing databases.
datetimeat store boundary(
TIMESTAMP WITH TIME ZONEon Postgres, ISO strings on SQLite).Phased plan
Phase 1 — make the change easy (SQLite-only, no behavior change):
extension point + migration framework. Mergeable independently.
Phase 2 — make the easy change: drop in
PostgresStore, CI matrix.Phase 3 — timestamp cleanup: proper datetime at store boundary.
Phase 4 — documentation.
Phase 1 dependency graph
After (1) lands, (2), (3), and (4) can proceed in parallel. (5) waits
on (3) + (4). (6) waits on (5). (7) waits on (2) being deployed
everywhere.
Child issues — Phase 1: protocol + Alembic, SQLite-only
Storeprotocol #306 Define asyncStoreprotocolSqliteStoreagainstStoreprotocol via Core helpers #308 ImplementSqliteStoreagainst protocol via Core helpersCQ_DATABASE_URLand store factory #309 WireCQ_DATABASE_URL+ store factory_ensure_*DDL now that Alembic owns the schema #310 Remove ad-hoc_ensure_*DDLChild issues — Phase 2: PostgresStore
postgresql+psycopgURL handling #311 Add psycopg v3 dependency +postgresql+psycopgURL handlingPostgresStoreagainstStoreprotocol #312 ImplementPostgresStorepg_advisory_lock(multi-pod safety) #313pg_advisory_lockwrap for migration startupChild issues — Phase 3: timestamp cleanup
datetimeat the store boundary #316 Datetime conversion at store boundaryTIMESTAMP WITH TIME ZONE#317TIMESTAMP WITH TIME ZONEon Postgres + caller updatesChild issues — Phase 4: documentation
docs/architecture.mdfor multi-backend support #318 Updatedocs/architecture.mdfor multi-backendCQ_DATABASE_URLin deployment / configuration docs #319 DocumentCQ_DATABASE_URLin deployment docsdocker-compose.yml#320 Optional Postgres service indocker-compose.yml