ADR-002: Drizzle ORM over Prisma / TypeORM
Status: Accepted
Context
The project needs a database abstraction layer for PostgreSQL. The main options considered were:
| ORM | Approach | Notes |
|---|---|---|
| Prisma | Schema-first DSL | Custom .prisma file, Prisma Client codegen, binary engine |
| TypeORM | Decorator-based | Heavy, class-based, full runtime reflection |
| Drizzle | TypeScript-first | Schema as TS code, SQL-like query builder, no runtime codegen |
Raw pg | No abstraction | Full control, but no type safety without manual interfaces |
Requirements:
- Types derived directly from the schema — no manual type definitions, no codegen that produces separate files
- Migrations checked into source control and reviewable as plain SQL
- Minimal runtime overhead
- Works well with Bun (no native addons that require Node.js-specific build steps)
- Query builder that is close to SQL so queries are predictable
Decision
Use Drizzle ORM with the postgres driver for all database access.
Key patterns enforced by convention:
// Schema defines the source of truth
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).notNull().unique(),
// ...
});
// Types are always inferred — never written by hand
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;Migration workflow:
- Edit or add a schema file in
backend/db/schemas/ bun run db:generate— Drizzle Kit generates a SQL migration file- Review the SQL in
backend/db/migrations/ bun run db:migrate— applies to the database
Consequences
Positive
- Types are always in sync with the schema —
$inferSelect/$inferInserteliminate the “type drift” problem - Migrations are plain SQL files: reviewable in PRs, replayable, not locked to a binary engine
- No Prisma binary engine to download or compile — faster CI and cleaner Docker images
- Query builder is SQL-shaped:
db.select().from(users).where(eq(users.id, id))reads like SQL - Zero runtime codegen: the schema file is just TypeScript imported at runtime
- Works natively with Bun — no
@prisma/enginesor native addon compilation
Negative / Risks
- Drizzle is newer than Prisma; the ecosystem of community tutorials and plugins is smaller
- Migrations are append-only SQL files — developers must not edit them manually (regenerate instead)
- Relations are defined separately from the table schema, which is a Drizzle convention that surprises Prisma users
- No built-in seeding or factory utilities —
backend/db/seed.tsis hand-rolled