ADR-0002: Self-contained store strategy¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-06-07 |
| Authors | Roman Mednitzer |
Context¶
praxis is the fleet's source of truth. It must persist a typed graph of
bitemporal facts (ADR-0003) and an append-only audit log (ADR-0008), be operable
on a single laptop with no external services, and scale to a production
deployment. The self-contained rule (ADR-0001) forbids a runtime dependency on
any external converged store.
Two competing pressures: the laptop case wants zero operational surface (one file, no daemon), the production case wants a real engine with a graph extension and vector search. A single hardcoded backend cannot serve both without either over-provisioning the laptop or under-serving production.
Decision¶
- Define a narrow
StoreProtocol(the L1 surface) that every backend honours:put_fact,supersede_fact,get_active,history, edges, and a capability probe. Service code depends only on the Protocol, never on a concrete backend. - Use an extension ladder. Optional capabilities (vector similarity, graph traversal, batch) are separate Protocols a backend advertises and implements only if it can honour them. A backend never fakes an unsupported capability.
- Ship SQLite as the default backend: a single file, bitemporal fact tables, an
append-only delete-blocking trigger, the active-fact unique constraint, and
embeddings via
sqlite-vecwhen present (degrading to no vector search when absent). No daemon, no network, works offline. - Ship a Postgres + Apache AGE + pgvector backend for production behind the same Protocol, as an optional extra. It is imported lazily; the package imports and type-checks with the driver absent.
- Bitemporality and append-only are enforced at the storage layer (a trigger or equivalent), not only in application code, so a direct write cannot bypass the invariant.
Consequences¶
Positive: the laptop and production cases share one code path; tests run against SQLite with no services; the production engine is swappable without touching service code; the append-only guarantee survives a buggy caller.
Negative: two backends to maintain; the Protocol is the lowest common denominator, so backend-specific power is reached only through capability Protocols.
Neutral: sqlite-vec and psycopg are optional extras, not core dependencies;
their absence degrades features, never breaks import.
Alternatives considered and rejected¶
- A single Postgres-only backend. Rejected: forces a daemon onto the single-operator laptop case and breaks offline operation.
- An ORM over both engines. Rejected: the bitemporal and append-only invariants are expressed most safely in raw SQL with a delete-blocking trigger; an ORM obscures the trigger and the active-fact constraint.
- Depending on the estate's existing converged graph store. Rejected by ADR-0001 (self-contained).
Revisit triggers¶
- The fleet outgrows SQLite for the default deployment.
- A third backend is required (for example, an embedded graph engine).
- A capability needed by service code cannot be expressed in the L1 Protocol.