docs/architecture/persistence-backend-boundary.md
OmniRoute currently presents domain-oriented persistence functions from src/lib/db/, while the
shared connection returned by src/lib/db/core.ts implements the synchronous SqliteAdapter
contract in src/lib/db/adapters/types.ts. That adapter supports several SQLite runtimes, but its
surface remains SQLite-shaped: synchronous prepared statements, pragma, deferred and immediate
transactions, native/file-copy backup, checkpoint, and a local database handle.
The current startup and recovery path also owns the SQLite file lifecycle. src/lib/db/core.ts
resolves storage.sqlite, maintains one process-global adapter, checkpoints WAL, preserves selected
tables during recovery, and removes SQLite companion files when rebuilding a database. Driver
selection in src/lib/db/adapters/driverFactory.ts chooses among the supported SQLite runtimes; it
is not an external-backend abstraction.
Schema evolution is similarly coupled. src/lib/db/migrationRunner.ts applies numbered SQL files,
probes sqlite_master and PRAGMA table_info, detects optional FTS5 support, and runs migration
work in SQLite transactions. Operational modules such as src/lib/db/backup.ts and
src/lib/db/optimizationSettings.ts use backup, PRAGMA, WAL, page-size, auto-vacuum, and VACUUM
semantics directly.
These are valid properties of the embedded SQLite deployment. They should remain available without forcing PostgreSQL or MySQL to emulate a SQLite API.
Adopt a two-level persistence boundary for portable durable state:
SQLite remains the default implementation. The existing SQLite driver cascade and synchronous
SqliteAdapter stay behind the SQLite repository implementation while domains are migrated in
small vertical slices. No user is required to configure an external service.
PostgreSQL is the first proposed external implementation after the repository boundary is proven against SQLite. MySQL follows as a peer implementation against the same conformance suite rather than as a second business-logic fork.
A portable repository may expose:
Backend health, readiness, and migration coordination belong to the internal backend/operational contract rather than to individual domain repositories.
A portable repository must not expose:
prepare, get, all, run, or raw driver handles;PRAGMA, WAL checkpoint modes, VACUUM, or page/cache tuning;lastInsertRowid as a cross-backend domain contract;sqlite-vec syntax;Backend-specific behavior remains explicit and discoverable. SQLite-only maintenance stays behind its own implementation and operational interface, including:
sqlite-vec integration.An external backend is not required to imitate those features. Repositories must either use a portable capability, provide a backend-specific implementation with documented behavior, or report that a capability is unavailable.
Repository APIs define the atomic business operation; callers do not select a SQL transaction mode. Each operation must define its observable concurrency guarantees: protected invariants, conflict detection, retry classification, idempotency expectations, and transaction-context propagation. Implementations may use different transaction and isolation mechanisms only when those observable guarantees remain equivalent. SQLite may continue using its current deferred or immediate transaction behavior internally where it satisfies the operation's contract.
External backends require explicit migration ownership so multiple application replicas cannot race the same schema change. Backend migration histories may share logical milestones, but SQLite SQL files are not assumed to be portable or reusable as another dialect.
Conformance tests must cover behavior, not only repository method signatures. Each migrated domain must define and verify:
NULL ordering, collation, and case-sensitivity expectations;If a domain cannot state equivalent observable semantics, it is not yet portable and must remain backend-specific until that contract is designed.
Any implementation following this ADR must preserve these properties:
Each runtime step is a separate, reviewable PR. A later step must not be used to justify merging an unproven abstraction in an earlier step.
The first runtime slice should be selected after the coupling inventory is reviewed. Provider
connections, API keys, combos, and routing configuration are candidates because their base tables
are visible in src/lib/db/core.ts, but this ADR does not approve a table list or a migration PR.
The slice must include:
SqliteAdapterRejected. SqliteAdapter is a compatibility layer for SQLite runtimes and exposes SQLite-specific
operations. Emulating that surface would leak synchronous and dialect-specific assumptions into a
new backend.
Rejected as the primary boundary. It would centralize connection handling but leave SQL dialect, transaction, and table coupling in business modules. A low-level backend primitive may exist inside repository implementations, not as the application-facing persistence API.
Rejected. The current persistence surface is broad and includes file lifecycle, recovery, search, and operational settings. Vertical slices provide reviewable behavior and rollback boundaries.
Rejected. Embedded and desktop deployments depend on the current zero-service startup model. An external backend is opt-in.
Rejected. Redis may support explicitly ephemeral coordination, cache, or counters, but it does not replace the durable repository contract described here.
This ADR does not:
sqlite-vec, backup files, or SQLite maintenance portable;src/lib/db/.Until these questions are resolved, this document is a proposal and no runtime refactor is implied.