Skip to Content
Living documentation — last reviewed 2026-05-28
DecisionsADR-0003: Drizzle ORM over Prisma / TypeORM

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 createDbClient returns a typed DbClient.
  • API injects via the DATABASE_CLIENT DI token (global DatabaseModule).

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 * from overhead like in some ORMs.

Negative

  • No automatic down-migrations. Rollback is forward-only or via DB snapshot restore. See runbooks/migrations.md.
  • The _journal.json when monotonicity 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:generate after editing schema, then review the SQL before applying.
  • Migrations through the pooler are forbidden — drizzle.config.ts uses DIRECT_DATABASE_URL.
  • Owner approval gates pnpm db:migrate in any non-throwaway environment.
  • Renames go through the two-step pattern (add new → backfill → drop old).
  • _journal.json when must be strictly monotonic. If it isn’t, regenerate.