Orbit
05 · Integrations

ORM: Prisma or Drizzle

One repository port per aggregate. Two interchangeable ORMs behind it. Picked once at scaffold time.

Orbit ships with two database access layers: Prisma (the default free track) and Drizzle (a paid track). Both sit behind the same repository interfaces in domain/, so the application and domain layers never import an ORM. Swapping between them is a scaffold-time choice — the CLI strips the one you didn't pick, and the generated project ships with a single, consistent stack.

Note

Scaffold-time, not runtime. The ORM choice isn't a runtime flag. When you run create-orb my-app --orm-provider=drizzle, the CLI deletes every Prisma file, fenced block, and dependency. Generated projects only carry the one ORM they use.

Note

Pick a track and the rest of the docs follow. Use the Prisma / Drizzle tabs on any page — your choice is remembered in this browser, so commands and code samples across the whole docs tree default to the ORM you picked. Switch at any time.

Picking an ORM

OptionTierWhen to pick it
prismaFree (default)Excellent type safety, DMMF-driven client, mature migrations, the path every Orbit guide assumes. Pick this unless you have a reason not to.
drizzlePaidSQL-first query builder, smaller runtime, closer to raw SQL for people who want it. Same repository surface area — no application-code changes.

CLI usage

ORM

scaffold with Prisma (default)

create-orb my-app
# or explicit
create-orb my-app --orm-provider=prisma

The --orm-provider flag maps to the orm.provider option in features.json. Picking drizzle enables the orm-drizzle sub-feature (paid) and strips orm-prisma, including the apps/api/prisma/ directory, the generated client, and every Prisma*Repository file.

What stays the same

Domain and application code is identical across both tracks — the repository interfaces are the contract, and they don't care which ORM implements them.

domain code runs unchanged on either track

await uow.run(async (tx) => {
const user = await tx.users.findById(userId);
if (!user) throw new Error("not found");
user.rename("New name");
await tx.users.save(user);
});
  • TxContext exposes the same repository ports (users, workspaces, …).
  • Domain events, projectors, and the UnitOfWork semantics are identical — both adapters extend a shared BaseUnitOfWork.
  • better-auth swaps @better-auth/prisma-adapter@better-auth/drizzle-adapter automatically — the adapter site is fenced in interfaces/http/better-auth.ts.

What differs

Schema source of truth

The prisma/schema.prisma file remains the single source of truth for column definitions even on the Drizzle track. The Drizzle schema in src/db/drizzle/schema.ts mirrors it one-to-one — when you add a column, update both files. This keeps the mental model simple: schema lives in one place, two ORMs know how to read it.

Migration commands

TaskPrismaDrizzle
Generate client / typesnpm run prisma:generateTypes are auto-inferred from src/db/drizzle/schema.ts
Create a migrationnpm run prisma:migratenpm run drizzle:generate
Apply pending migrationsnpm run prisma:migratenpm run drizzle:migrate
Reset local DBnpm run prisma:resetdrizzle-kit drop + re-run migrate
Inspect in GUInpx prisma studionpm run drizzle:studio

ID generation

Prisma uses a $extends client hook to auto-fill prefixed UUIDv7 ids on create / createMany. Drizzle doesn't have an equivalent, so Drizzle repositories call newId("user") (etc.) explicitly before db.insert(...). The net effect is the same — every row gets a typed, prefixed id — but the mechanism is explicit on the Drizzle side.

Under the hood

apps/api/src/kernel/base-uow.ts — the shared base

export abstract class BaseUnitOfWork<TxHandle> implements UnitOfWork {
constructor(protected readonly bus: EventBus) {}
protected abstract openTransaction<T>(fn: (h: TxHandle) => Promise<T>): Promise<T>;
protected abstract buildContext(handle: TxHandle): RepoContext;
protected abstract readHandle(): TxHandle;
async run<T>(fn: (tx: TxContext) => Promise<T>): Promise<T> { /* shared */ }
async read<T>(fn: (tx: TxContext) => Promise<T>): Promise<T> { /* shared */ }
}

Both PrismaUnitOfWork and DrizzleUnitOfWork extend this class and supply three small methods: how to open a transaction, how to build a repository context from a transactional handle, and how to return a non-transactional handle for reads. Event collection, read-only guards, and post-commit dispatch are inherited — the semantics are guaranteed identical between the two.

Files added / changed by the Drizzle strip

  • apps/api/drizzle/ — drizzle-kit config and migrations folder.
  • apps/api/src/db/drizzle/schema.ts — schema definitions mirroring the Prisma models.
  • apps/api/src/infrastructure/drizzle.ts and drizzle-uow.ts — client + unit of work.
  • apps/api/src/**/infrastructure/drizzle-*.repository.ts — one file per repository interface.
  • npm scripts: drizzle:generate, drizzle:migrate, drizzle:push, drizzle:studio.

Switching from Prisma to Drizzle later

If you scaffolded with Prisma and want to move to Drizzle on an existing project, there's no "swap" command — scaffold a fresh project with --orm-provider=drizzle, point it at your existing database, and copy over your domain, application, and interface code (none of which depend on the ORM). Drizzle's drizzle-kit pull can introspect an existing database if you want to verify the schemas match.

Note

Both tracks share these tests. The test suite in apps/api/src/**/*.test.ts exercises services through UnitOfWork so the same tests cover both ORM paths — the adapter under the port is irrelevant.