For a TanStack Start SaaS, use Drizzle unless you have a specific reason to pick Prisma. Drizzle has a small bundle, no client-generation step and no native binary, and its types update as soon as you save the schema. Choose Prisma if your team prefers a schema-first .prisma file, wants Prisma Studio, or is porting an existing Prisma codebase. Both ORMs work with TanStack Start, and the MakerKit TanStack Start kit ships in both variants, with Drizzle as the default.
The choice differs from a plain Node server in two ways. TanStack Start builds through Vite and Nitro, so the same app can deploy to a Node server, to serverless functions or to edge runtimes. And your database code runs inside createServerFn handlers rather than route handlers.
This post covers only what changes when the framework is TanStack Start. For schema syntax, query APIs and performance in general, read the Drizzle vs Prisma comparison. If you are new to the framework, start with what is TanStack Start; if you are still choosing a framework, see TanStack Start vs Next.js.
TL;DR
- Both ORMs work with TanStack Start. In MakerKit they live in
packages/database, and nothing else in the kit changes between variants. - Drizzle is about 7.4 KB min+gzip with zero dependencies, has no generate step and no binary, and infers types directly from your TypeScript schema.
- Prisma 7 removed the Rust query engine, so it now deploys to the same serverless and edge targets. It still has a larger client and requires
prisma generate. - Pick Drizzle for SQL-shaped queries, the smallest cold-start cost and no build step. Pick Prisma for schema-first modeling, Prisma Studio, or an existing Prisma codebase.
- MakerKit's TanStack Start kit defaults to Drizzle. The Prisma variant differs only in
packages/database.
Drizzle vs Prisma in TanStack Start: the comparison table
The table lists the differences that matter 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 |
In the MakerKit kits, everything outside the ORM is identical between the two variants: Better Auth, billing, the organization and teams model, and role-based access control. Only packages/database differs. That makes the ORM choice reversible and much cheaper to get wrong than the framework choice.
Where the ORM lives in a TanStack Start kit
Both kits export a single db client from @kit/database, and server functions import it from there. The Drizzle kit's package entry point is short:
// 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 is a postgres-js connection wrapped by Drizzle. The prepare: false option is required for transaction-mode connection pooling, the same setting you need 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 replaces this file with a PrismaClient built on the @prisma/adapter-pg driver adapter over a pg pool:
// packages/database/src/adapters/postgres.ts — TanStack Start 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 });}In Prisma 7 the generated client is written to your source tree (src/prisma/generated/client), not to node_modules. This affects the build order, covered below.
Which ORM has smaller cold starts on Nitro serverless and edge?
Drizzle. It loads less code per cold invocation, and the gap only matters on serverless and edge targets.
TanStack Start builds through Nitro, and you choose the deploy target: a long-running Node server, a serverless function (Vercel, AWS Lambda) or an edge runtime. On a long-running Node server, cold starts are rare and either ORM is fine. On serverless and edge, the runtime loads and initializes your bundle on every cold invocation, so the ORM's size adds directly to cold-start latency.
Drizzle is about 7.4 KB min+gzip with zero dependencies and has no native binary. At runtime it adds little beyond your own code and the Postgres driver. This is the main reason MakerKit uses it as the default for TanStack Start.
Prisma 7 removed the Rust query engine, so the old rule that Prisma can't run on the edge no longer applies. The client is pure TypeScript and deploys to serverless and edge targets that earlier versions could not run on. The client is still much larger than Drizzle's, so each cold start initializes more code. On a warm Node server you won't notice. If you are trying to keep serverless cold starts low, you will.
In short, Prisma 7 can now run everywhere Drizzle can, but it still ships more code. TanStack Start is built around fast Vite builds and multiple deploy targets, and the smaller client fits that better.
Does Prisma need a build step in TanStack Start?
Yes. Prisma needs prisma generate before your Vite build and before typecheck. Drizzle has no client to generate: the schema is TypeScript, so the types are current as soon as you save schema.ts.
If prisma generate doesn't run first, the generated client is stale or missing. The Next.js Prisma kit runs it from the package's 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 client-generation script. Its scripts only handle migrations and Drizzle Studio:
// 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"}The names overlap: drizzle-kit generate writes migration SQL files. It does not generate a client.
The generate step is a common source of "why are my types wrong?" questions on the Prisma side. The usual cause is an edited schema without a regenerate. Each regeneration takes seconds, but you repeat it after every schema change.
Prisma 8 is in release candidate as of September 2026, with general availability expected in October 2026 according to Prisma's release status page. It replaces client generation with a lighter "contract emit" step, so a build step remains. The MakerKit Prisma kits use Prisma 7.
Migrations workflow
Both ORMs generate SQL migration files, and in both kits migrations run from packages/database, independent of the app framework.
Drizzle uses drizzle-kit. The kit uses timestamp-prefixed migration filenames so kit migrations and your own never share a sequence number:
// 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,});With strict: true, drizzle-kit asks you to confirm ambiguous changes such as a column rename instead of treating them as a drop plus an add, which would lose the column's data. Keep it on.
Prisma uses prisma migrate dev, configured in 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 depends on TanStack Start. Because migrations live in packages/database, you can change and test the schema without starting the web app.
Type safety inside TanStack Start server functions
Both ORMs give you full type inference inside a createServerFn handler or the service it calls. The difference is when the types update: Drizzle's on save, Prisma's after prisma generate.
Here is a service from the Drizzle kit that an admin server function calls. It runs two counts in parallel:
// 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 contains no query code. It delegates to the loader, which calls the service:
// 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(),);In the Prisma variant the server function is the same. Only the service changes: db.user.count() and db.user.count({ where: { banned: true } }) replace the Drizzle select calls.
Raw SQL access
Drizzle's sql template tag works inside the query builder, so you can mix raw SQL into a typed query. Prisma's $queryRaw and $executeRaw are separate methods on the client.
Most SaaS apps eventually need a query the ORM API doesn't cover, such as a window function, a recursive CTE or a hand-tuned aggregate. With Drizzle, you add the raw expression to 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);With Prisma, you write the whole query in $queryRaw and type the result yourself. If your codebase uses raw SQL often, Drizzle keeps it in the same API as your other queries. If you rarely write raw SQL, this difference shouldn't decide anything.
When to choose Drizzle for TanStack Start
- You deploy to Nitro serverless or edge targets and want the lowest cold-start cost.
- You want no build step: no client generation, types current on save.
- You are comfortable with SQL and want queries that map directly to it.
- You write raw SQL often and want it inside typed queries.
- You want MakerKit's default TanStack Start setup with the least configuration.
When to choose Prisma for TanStack Start
- You prefer schema-first modeling in a
.prismafile over TypeScript schema objects. - You want Prisma Studio for browsing data during development.
- You are porting an existing Prisma codebase and don't want to rewrite every query.
- Your team prefers Prisma's object-based query API to SQL-shaped queries.
If you already test Prisma code, our guide to unit testing Prisma with pgLite and Vitest applies to the Prisma variant.
What MakerKit defaults to
The MakerKit TanStack Start kit uses Drizzle by default. TanStack Start is a Vite + Nitro framework built around typed server functions, and Drizzle's lack of a generate step, lack of a native binary and small bundle fit that model with the least setup, especially on serverless and edge targets. The code samples above come from that kit.
The kit also ships a Prisma variant. As with our Next.js Drizzle and Prisma kits, the two differ only in packages/database; Better Auth, billing, teams and RBAC are shared. For how auth is wired in both, see Better Auth with TanStack Start.
Switching later is possible. Because the ORM sits behind @kit/database and is only called from services, switching means rewriting packages/database and updating the query calls in your services. Routes, server functions and UI stay the same.
To get started, read the TanStack Start Drizzle kit docs or the TanStack Start Prisma kit docs.