Skip to Content
PersistenceOverview

Persistence

packages/persistence is the database layer of the OpsDNA monorepo: it owns the Postgres schema (via a custom SQL migration system), the Kysely repositories every app and service goes through, the row/adapter seam that keeps storage shapes out of the fund-economics runtime, and the role/RLS posture that makes every query tenant-isolated by construction.

If you are about to touch a table, a foreign key, or anything that looks like a schema decision, stop and read DATA-MODEL.md first — it is the canonical entity model, and persistence is where its invariants are physically enforced. This page gives you the mental model of the package; the sibling pages are task-oriented: writing and running migrations, the repository/row-adapter contract, roles and RLS, and testing against pglite vs real Postgres.

Why the layer looks the way it does

Two forces shaped this package. First, OpsDNA is fund accounting: correctness is non-negotiable, so the schema is heavily constrained (FORCE row-level security on 346 of 354 tables, SECURITY DEFINER writers, closed enums, checksum-locked migrations) rather than convenient. Second, the runtime that computes fund economics must never depend on how results are stored — that separation is ratified in ADR-0001 and ADR-0002 and enforced structurally: runtime emits canonical <Domain>Application records, and only the adapter directory in persistence may translate them into <Domain>ApplicationRow projections.

Package map

WhereWhat
packages/persistence/baselines/0001_adr0025_namespace_baseline.sqlThe ADR-0025 squashed baseline — the entire pre-cutover schema in one file, applied only to fresh databases
packages/persistence/post-baseline/The active migration directory. New forward migrations go here (0002_… onward)
packages/persistence/migrations/The frozen pre-cutover legacy chain (256 files). Historical replay tests only — never add product migrations here
packages/persistence/src/migrator.tsThe custom Effect-adjacent migrator: checksum ledger, per-migration transactions, fresh-baseline fast path
packages/persistence/src/cli/migrate.tsCLI: migrate:latest, migrate:up, migrate:status
packages/persistence/src/*-repository.tsKysely repositories (one file per aggregate; tenant-scoped via withTenantTransaction)
packages/persistence/src/adapters/The only place row types meet canonical fund-economics records (ADR-0002)
packages/persistence/src/schema-registry.tsThe machine-readable schema-ownership manifest (every table’s schema, RLS policy, id family)
docs/schema-atlas.htmlThe browsable, human-friendly render of that manifest — table-by-module plus wiring recipes
packages/postgres-test-harness/acquireTestDatabase() — pglite (default) or real Postgres, same handles either way

The three ideas to internalize

Migrations are forward-only and checksum-locked. The migrator stores sha256 of each applied file in public.opsdna_migrations; editing an applied file makes every future deploy fail. See Migrations.

Storage shapes are projections, not the source of truth. A …ApplicationRow is a narrowed view of a canonical record, produced by an explicit adapter with a PROJECTED_FIELDS list checked by the compiler. See Repositories.

Tenant isolation is a database property, not an application habit. The app connects as opsdna_app, a role that cannot bypass RLS; sensitive writes go through SECURITY DEFINER functions owned by dedicated resolver roles. See Roles & RLS.

New to the repo? Start with Getting started, then come back here before your first schema change.

Last updated on