You picked TanStack Start. Now you have to pick an ORM. The two real options are Drizzle and Prisma, and the decision plays out differently here than it does in a plain Node server, because TanStack Start ships a Vite + Nitro pipeline that can deploy to Node, to serverless functions, or to edge runtimes, and your data layer runs inside server functions rather than route handlers.
Quick answer: For a TanStack Start SaaS, default to Drizzle. It has a tiny bundle, no code-generation step, no native binary, and it drops straight into a server function with instant types. Choose Prisma if your team prefers the schema-first model, wants Prisma Studio, or is porting an existing Prisma codebase. Both work. Drizzle is the lower-friction fit for the Vite + Nitro deploy-anywhere model, which is why the MakerKit TanStack Start kit defaults to Drizzle.
This post is about the choice inside a TanStack Start kit. If you want the full head-to-head on schema syntax, query APIs, and performance internals, read the general Drizzle vs Prisma comparison first. This one stays narrow: what actually changes when the framework is TanStack Start. New to the framework? Start with what is TanStack Start, and if you're weighing frameworks, see TanStack Start vs Next.js.
TL;DR
- Both ORMs work with TanStack Start. They live in
packages/database; nothing else in the kit changes. - Drizzle has a ~12 KB (min+gzip) footprint, no generate step, no binary, and instant type inference. It's the cleaner fit for Nitro serverless/edge targets and the framework's server-function model.
- Prisma 7 is pure TypeScript now (Rust engine gone), so it deploys to those same targets, but it carries a larger client and a required
prisma generatestep. - Pick Drizzle if you want SQL control, smallest cold starts, and zero build friction. Pick Prisma if you want schema-first modeling, Prisma Studio, or you're migrating existing Prisma code.
- MakerKit defaults to Drizzle for its TanStack Start kit, with a Prisma variant that is architecturally identical apart from
packages/database.
The decision, TanStack-Start-specific
The general comparison covers the ORMs in the abstract. Here's what actually differs once TanStack Start is the framework:
| Dimension in a TanStack Start kit | Drizzle | Prisma 7 |
|---|---|---|
| Fit with Nitro serverless/edge targets | Native. No binary, tiny bundle | Works since v7 (Rust removed); larger client |
| Cold start impact | Minimal | Improved in v7, still heavier |
Build step before vite build | None | prisma generate required |
| Type inference in server functions | Instant on save | After generate |
| Client shape | Query builder + schema objects | Generated client in your source tree |
| Raw SQL inside a server fn | sql template tag, native | $queryRaw / $executeRaw |
| Where it lives in the kit | packages/database | packages/database (Prisma variant) |
| What else changes | Nothing | Nothing |
Everything above the ORM line is identical between the two kit variants: Better Auth, billing, the organization and teams model, and role-based access control are shared. Only packages/database differs. That's not marketing; it's the actual architecture, and it's why this decision is reversible and low-stakes compared to picking the framework itself.
Where the ORM lives in a TanStack Start kit
Both kits expose a single db client from @kit/database, and your server functions import it. The Drizzle kit's export surface is small:
// packages/database/src/index.ts — TanStack Start Drizzle kitexport { db, type DatabaseSchema } from './client.server';export * from './schema/schema';// export auth context utilitiesexport * from './auth';The client itself is a plain postgres-js connection wrapped by Drizzle, with a prepare: false flag that matters for transaction-mode connection pooling (the same gotcha you hit with Supabase's pooler):
// packages/database/src/adapters/postgres.ts — Drizzle kitimport { PostgresJsDatabase, drizzle } from 'drizzle-orm/postgres-js';import postgres from 'postgres';import { databaseUrl } from '../database-url.server';import * as schema from '../schema/schema';export type DatabaseSchema = typeof schema;function createPostgres() { // Disable prefetch as it's not supported for "Transaction" pool mode return postgres(databaseUrl, { prepare: false, max: Number(process.env.DATABASE_POOL_MAX) || 20, });}The Prisma variant swaps this file for a Prisma client built on the @prisma/adapter-pg driver adapter, the same way the Next.js Prisma kit wires it:
// packages/database/src/adapters/postgres.ts — Next.js Prisma kitimport { PrismaPg } from '@prisma/adapter-pg';import { Pool } from 'pg';import { databaseUrl } from '../database-url';import { PrismaClient } from '../prisma/generated/client';function createPrismaClient() { const pool = new Pool({ connectionString: databaseUrl }); const adapter = new PrismaPg(pool); return new PrismaClient({ adapter });}Note that in Prisma 7 the generated client lives in your source tree (src/prisma/generated/client), not in node_modules. That's a real change and it matters below.
Cold starts and edge, on Nitro targets
This is the dimension where TanStack Start makes the choice more pointed than a plain Node deploy.
TanStack Start builds through Nitro, so you choose a deploy target: a long-running Node server, a serverless function (Vercel, AWS Lambda), or an edge runtime. On the long-running Node target, cold starts barely matter and either ORM is fine. On serverless and edge, the ORM's footprint feeds directly into cold-start latency, because the runtime has to load and initialize your bundle per cold invocation.
Drizzle is roughly 12.2 KB min+gzip with zero external dependencies and no native binary. There's nothing to load beyond your own code and the Postgres driver. That's why it's the default here.
Prisma 7 removed the Rust query engine, so the old "Prisma can't run on the edge" rule is gone. The client is pure TypeScript now and deploys to serverless and edge targets that earlier Prisma versions couldn't touch. But the client is still substantially larger than Drizzle's, so on a per-cold-start basis it initializes more code. If your target is a warm Node server, this is noise. If you're chasing sub-200ms cold starts on serverless functions, it's a measurable tax.
The honest version: Prisma 7 closed the capability gap on edge, but not the footprint gap. For a framework whose whole pitch is Vite speed and deploy-anywhere output, the leaner client is the more consistent choice.
Bundle size and the build step
Two related things. Drizzle adds almost nothing to your server bundle and, crucially, has no generation step. Your schema is TypeScript, so the moment you save schema.ts the types are current. There's nothing to run before vite build.
Prisma requires prisma generate to produce the client. In a TanStack Start project that means the generate step has to run before your Vite build and before typecheck, or the generated client is stale or missing. The Next.js Prisma kit wires this into a build script:
// packages/database/package.json — Prisma kit"scripts": { "build": "prisma generate", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev"}The Drizzle kit has no equivalent generate step; its scripts are only about migrations:
// packages/database/package.json — Drizzle kit"scripts": { "drizzle:generate": "drizzle-kit generate --config drizzle.config.mjs", "drizzle:migrate": "drizzle-kit migrate --config drizzle.config.mjs", "drizzle:studio": "drizzle-kit studio --config drizzle.config.mjs"}Note the naming overlap: drizzle-kit generate generates migration SQL, not a client. There's no client to generate. That's the whole point.
The practical cost of the Prisma generate step is the single most common support question we see on the Prisma side: "why are my types wrong?" The answer is almost always "you edited the schema and didn't regenerate." It's a small tax per change, and it adds up over a day of schema iteration.
Migrations workflow
Both ORMs produce SQL migration files. The workflows differ, and both slot into the kit's tooling the same way regardless of framework.
Drizzle uses drizzle-kit. The kit configures timestamp-prefixed migrations so upstream kit migrations and your own never collide on a filename index:
// packages/database/drizzle.config.mjs — Drizzle kitexport default defineConfig({ schema: './src/schema/schema.ts', out: './src/schema', dialect: 'postgresql', migrations: { prefix: 'timestamp' }, schemaFilter: ['public'], verbose: true, strict: true,});strict: true is doing real work here. It prompts you when Drizzle detects an ambiguous change like a column rename, instead of silently interpreting it as drop-plus-add and losing data. Keep it on.
Prisma uses prisma migrate dev, configured through prisma.config.ts:
// packages/database/prisma.config.ts — Prisma kitexport default { schema: path.join('src', 'prisma', 'schema.prisma'), migrations: { path: path.join('src', 'prisma', 'migrations') }, datasource: { url: process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54333/postgres', },} satisfies PrismaConfig;Neither workflow is TanStack-Start-specific. Migrations run at the packages/database level, decoupled from the app framework. That's a feature: you can develop and test your schema without booting the web app.
Type-safety ergonomics inside server functions
In TanStack Start, your data access happens inside a createServerFn handler (or a service the handler calls). Both ORMs give you full type inference at that point. The difference is when the types are current.
With Drizzle, they're current the instant you save. Here's a real service from the kit, called behind an admin server function, running parallel counts:
// packages/admin/.../admin-dashboard.service.ts — Drizzle kitimport { count, eq } from 'drizzle-orm';import { db, user } from '@kit/database';async getAdminStats() { const [totalUsersResult, bannedUsersResult] = await Promise.all([ db.select({ count: count() }).from(user), db.select({ count: count() }).from(user).where(eq(user.banned, true)), ]); return { totalUsers: totalUsersResult[0]?.count ?? 0, bannedUsers: bannedUsersResult[0]?.count ?? 0, };}The server function itself is thin. It validates input and delegates to the service, which imports db:
// apps/web/src/lib/admin.functions.ts — Drizzle kitimport { createServerFn } from '@tanstack/react-start';import { loadAdminStats } from '@kit/admin/dashboard/loader';export const fetchAdminStats = createServerFn({ method: 'GET' }).handler(() => loadAdminStats(),);The pattern with the Prisma variant is identical at the server-function layer. The only change is inside the service: db.user.count() and db.user.count({ where: { banned: true } }) instead of the Drizzle select. Same handler, same delegation, different query dialect. The type story diverges only in that Prisma's types come from the generated client, so they lag until prisma generate runs.
Raw SQL access
Every SaaS eventually needs a query the ORM won't express cleanly: a window function, a recursive CTE, a hand-tuned aggregate. Both ORMs give you an escape hatch inside a server function.
Drizzle's sql template tag composes with the query builder, so you can drop raw SQL into an otherwise typed query:
import { eq, sql } from 'drizzle-orm';const rows = await db .select({ org: organization, memberCount: sql<number>`count(distinct ${member.userId})`, }) .from(organization) .leftJoin(member, eq(member.organizationId, organization.id)) .groupBy(organization.id);Prisma exposes $queryRaw and $executeRaw. They work, but they're a separate API from the main client rather than a composable part of it. For a codebase that reaches for SQL regularly, Drizzle's approach has less context-switching, because the ordinary query builder already reads like SQL. If you rarely touch raw SQL, this dimension won't move your decision.
Pick Drizzle if...
- You're deploying to Nitro serverless or edge targets and want the smallest cold starts.
- You want zero build friction: no generate step, types current on save.
- You're comfortable with SQL and want the query builder to map onto it directly.
- You reach for raw SQL often and want it composable with typed queries.
- You want the path MakerKit's TanStack Start kit is already on, with the least setup.
Pick Prisma if...
- You prefer schema-first modeling in a dedicated
.prismafile over TypeScript schema objects. - You want Prisma Studio for browsing data during development.
- You're porting an existing Prisma codebase and don't want to rewrite every query.
- Your team is more frontend-oriented and likes Prisma's object-based query API over SQL-shaped queries.
If you're genuinely torn on the ORM itself rather than the framework, the general Drizzle vs Prisma comparison goes deeper on schema syntax, query APIs, and the Prisma 7 architecture change.
What MakerKit defaults to
The MakerKit TanStack Start kit ships on Drizzle. That's a deliberate call, not a coin flip. For a Vite + Nitro framework built around type-safe server functions, Drizzle's no-generate, no-binary, tiny-footprint model is the lower-friction fit, and it keeps cold starts honest across Nitro's serverless and edge targets. The receipts above are pulled from that real kit.
The TanStack Start kit ships in both Drizzle and Prisma variants. They are architecturally identical apart from packages/database, exactly as our Next.js kits mirror each other. The choice is a swap of one package, because Better Auth, billing, teams, and RBAC are shared across variants.
Whichever you pick, you're not locked in. The ORM lives behind @kit/database and gets used through services and server functions, so switching is a packages/database rewrite plus updating query calls, not a re-architecture. Most teams pick one and stay. Both are production-ready.
Want to see it running? Check the TanStack Start Drizzle kit docs and pull the repo.