ADR-0003: Drizzle ORM over Prisma / TypeORM
Status: Accepted Date: ~2026-01 (estimate) Context owner: Owner
Context
FitKit’s data model is non-trivial:
- ~40 schema files in
libs/db/src/lib/schema/. - Heavy use of enums (status fields, roles, payment states).
- pgvector embeddings for the exercise library.
- Multi-org tenancy where every read/write must scope by
organization_id. - A long lifetime ahead, so the ORM choice persists.
Decision
Use Drizzle ORM with PostgreSQL. Migrations are SQL files generated by drizzle-kit, reviewed in PRs, and applied via pnpm db:migrate. Never pnpm db:push.
- Schema in
libs/db/src/lib/schema/*.ts, organized by domain (workouts.ts,payments.ts,forms.ts, …). - One barrel export in
libs/db/src/lib/schema/index.ts. - Client factory
createDbClientreturns a typedDbClient. - API injects via the
DATABASE_CLIENTDI token (globalDatabaseModule).
Consequences
Positive
- The generated SQL is reviewable. No “magic” — when the migration is wrong, you can see it.
- No code generation step: schema files ARE the source of truth.
- Pure TypeScript inference. No runtime adapter introspection.
- pgvector works natively (
_pgvector.ts). - Cheap to construct ad-hoc queries; no
select * fromoverhead like in some ORMs.
Negative
- No automatic down-migrations. Rollback is forward-only or via DB snapshot restore. See runbooks/migrations.md.
- The
_journal.jsonwhenmonotonicity gotcha caused a production outage (2026-04-18). Discipline documented in migrations.md. - More boilerplate than Prisma for simple cases (you write
db.select().from(table)even for trivial selects). - Editor IDE plugins are less mature than Prisma’s.
Alternatives considered
- Prisma. Excellent DX but the Rust query engine adds a deploy artifact, the migration runner is opaque (until you hit edge cases), and pgvector support has historically been weak. The ORM also makes ad-hoc joins clunky.
- TypeORM. Less type-safe, decorators add runtime cost, the schema-vs-code dual source of truth is a foot-gun.
- Kysely + a separate migration tool. Lower-level but loses Drizzle’s first-class schema introspection.
Discipline
pnpm db:generateafter editing schema, then review the SQL before applying.- Migrations through the pooler are forbidden —
drizzle.config.tsusesDIRECT_DATABASE_URL. - Owner approval gates
pnpm db:migratein any non-throwaway environment. - Renames go through the two-step pattern (add new → backfill → drop old).
_journal.json whenmust be strictly monotonic. If it isn’t, regenerate.
Related
- runbooks/migrations.md
- architecture/database.md
- CLAUDE.md “Database Policy” section