Drizzle is a TypeScript-first SQL toolkit for Postgres, MySQL, SQLite, and Cloudflare D1. You declare your schema in TypeScript, write queries that look like the SQL you'd write by hand, and get full type inference without a codegen step. Zero runtime dependencies, ~7 kB min+gz, and first-class support for edge runtimes — Workers, Bun, Deno, Neon's serverless driver. The tradeoff Prisma makes for ergonomics, Drizzle refuses: the query you write is the SQL that runs.
Drizzle is two things in one package. drizzle-orm is a tiny, tree-shakeable query builder that compiles TypeScript expressions to SQL at build time — no reflection, no runtime codegen, no query engine binary. drizzle-kit is the CLI that handles migrations: it diffs your TypeScript schema against the database and generates SQL migration files you commit to git.
The mental model is simple. You describe your tables in a schema.ts file using Drizzle's column helpers. You pass that schema to a drizzle() factory with a driver of your choice. The resulting client is fully typed against your schema — db.select().from(users).where(eq(users.email, email)) knows users.email exists, knows it's a string, and compiles to one SQL statement. When the schema changes, drizzle-kit generate emits a migration; drizzle-kit migrate applies it.
Most 2nth production systems still sit on Frappe — doctypes are the ORM, and the DB is Postgres behind ERPNext. But when a build needs a custom Cloudflare Worker with its own tables (storefront caches, event logs, agent context stores, webhook inboxes), Frappe is the wrong tool. Drizzle is the right one: it runs inside the Worker, it works against D1 natively, and when a Worker talks to a bigger Postgres through Hyperdrive, the same Drizzle code works unchanged. One schema language, one migration pipeline, four database targets.
Most TypeScript ORMs make two choices that hurt on the edge. They ship a query engine as a native binary (Prisma's Rust engine, historically), and they lean on runtime reflection to stay generic. Both make cold starts slower, bundles bigger, and deployment to V8 isolates awkward or impossible.
Drizzle takes the opposite bet. Schema lives in TypeScript. Types are inferred, not generated. Queries are composed as values, not strings. SQL is emitted at the call site, not by an external process. The whole library is roughly seven kilobytes of JavaScript with no native dependencies — which is why it runs in Workers, Pages Functions, Bun, Deno Deploy, and Lambda with no adapter layer in between.
| Concern | Prisma | Drizzle |
|---|---|---|
| Schema source | Custom .prisma DSL | TypeScript file |
| Type generation | Codegen step — prisma generate | Pure type inference — no step |
| Query engine | Rust binary (historically); now WASM/TS | No engine — it's a query builder |
| Edge / Workers | Supported via driver adapters | First-class, from day one |
| Bundle size | Large — client + engine | ~7 kB min+gz |
| SQL control | Abstracted away; escape via $queryRaw | You write SQL-shaped TypeScript |
| Migration model | Schema-as-source-of-truth | Schema-as-source-of-truth — plus optional pull from DB |
Frappe has doctypes. PlatformIO has environments. Drizzle has the schema file. One TypeScript module declares every table, every column, every relation — and it's the same file the type system reads, the migration tool diffs, and the query builder consumes.
// schema.ts — Furniture Guild storefront cache (Worker + D1) import { sqliteTable, text, integer, real } from "drizzle-orm/sqlite-core"; import { relations, sql } from "drizzle-orm"; export const products = sqliteTable("products", { id: text("id").primaryKey(), sku: text("sku").notNull().unique(), name: text("name").notNull(), priceZar: real("price_zar").notNull(), stock: integer("stock").notNull().default(0), updatedAt: integer("updated_at", { mode: "timestamp" }) .default(sql`(unixepoch())`), }); export const productImages = sqliteTable("product_images", { id: text("id").primaryKey(), productId: text("product_id").notNull() .references(() => products.id, { onDelete: "cascade" }), url: text("url").notNull(), alt: text("alt"), }); export const productsRelations = relations(products, ({ many }) => ({ images: many(productImages), }));
One schema, one migration set, one typed client. On the Worker side:
// worker.ts — storefront endpoint running on D1 import { drizzle } from "drizzle-orm/d1"; import { eq } from "drizzle-orm"; import * as schema from "./schema"; export default { async fetch(req: Request, env: Env) { const db = drizzle(env.DB, { schema }); const sku = new URL(req.url).pathname.split("/").pop()!; // Relational query — JOIN generated for you, fully typed const product = await db.query.products.findFirst({ where: eq(schema.products.sku, sku), with: { images: true }, }); return product ? Response.json(product) : new Response("not found", { status: 404 }); }, };
Switching targets is a one-line change. Swap drizzle-orm/d1 for drizzle-orm/postgres-js and the same db.query.products.findFirst(...) runs against a Postgres over Hyperdrive. The schema file moves too — sqliteTable becomes pgTable, column types shift where dialects differ — but the query surface is identical.
SQL-like: db.select().from(users).leftJoin(posts, ...).where(...) — explicit, composable, reads like the SQL it compiles to. Best when you want control.
Relational (Queries): db.query.users.findMany({ with: { posts: true } }) — nested object result, joins generated for you. Best when you want ergonomics.
Both live in the same client and use the same schema. Use whichever fits the call; don't pick one globally.
Drizzle ships as a small surface area of packages. Each one has a clear role and earns its place; nothing is bundled you don't import.
| Package / tool | What it does |
|---|---|
drizzle-orm | The query builder and type-inference core. Import the dialect-specific entry point (/pg-core, /mysql-core, /sqlite-core). |
drizzle-kit | CLI for migrations. generate diffs schema vs. last snapshot; migrate applies; push syncs directly (dev only); pull introspects an existing DB. |
| Drizzle Studio | Local browser-based data explorer. drizzle-kit studio spins it up — read/edit rows, run queries, inspect schema. Equivalent to Prisma Studio; runs offline. |
| Dialect drivers | postgres-js, node-postgres, @neondatabase/serverless, mysql2, @planetscale/database, better-sqlite3, bun:sqlite, Cloudflare D1 binding, @libsql/client (Turso), Expo SQLite. |
| Relations API | Define one/many relations alongside tables; use db.query.X.findMany({ with: ... }) for nested loads without writing joins. |
| Zod / Valibot / Yup adapters | drizzle-zod generates insert/select validators from your schema. One source of truth across API boundary validation and DB layer. |
drizzle-seed | Deterministic seed data generator that respects your schema — relations, constraints, and all. |
Furniture Guild's storefront fetches product data from ERPNext on origin, but a hot path — product pages, category lists, search — runs off a D1 cache updated by scheduled Workers. Drizzle models the cache tables, D1 is the store, the whole thing lives in one Worker bundle. No ORM for Python to reach across; ERPNext stays the system of record.
When the Worker needs real-time writes against a shared Postgres — webhook ingest, agent memory, audit logs — point the Postgres driver at the Hyperdrive binding's connection string. Drizzle doesn't care that it's pooled. Same schema.ts, same queries, connection overhead handled by Cloudflare's network.
Long-running agents need durable state: conversation history, tool-call logs, task queues. Drizzle + D1 gives you a typed interface over per-agent or per-tenant SQLite without standing up a new database service. For bigger workloads, the same schema moves to Postgres with a driver swap.
Not every 2nth service is a Worker. A Bun service doing PDF ingest or a Deno task runner benefits from the same ORM discipline. Drizzle works natively in both — bun:sqlite driver, Deno-compatible packages — so the SQL layer is consistent across runtimes.
Drizzle launched into a crowded field and carved space by refusing to make the usual tradeoffs. In three years it went from a curiosity to the default TypeScript ORM for serverless and edge builds.
| Era | What shipped | What it unlocked |
|---|---|---|
| 2022 | Drizzle ORM v0 — SQL-like query builder for Postgres | Proof that TS inference could replace codegen |
| 2023 | MySQL + SQLite dialects, drizzle-kit migrations, D1 adapter | One ORM for every SQL target that matters on the edge |
| 2024 | Relations API, Drizzle Studio, Turso + Neon serverless drivers | Ergonomic queries without giving up edge-first design |
| 2025 | drizzle-seed, RQB v2, stronger multi-dialect schema helpers | Seeds, nested queries, and schema-as-TS matured into a serious production toolkit |
| 2026 | Widely adopted as the default Workers/D1 ORM; strong Bun and Expo support | Standard library status in the TS-on-edge stack |
Drizzle is not the right tool for every 2nth build. If the system of record is Frappe, don't reach for Drizzle — reach for doctypes. If the service is Python, don't polyglot the persistence layer for its own sake.
env.DB.prepare()drizzle-zod).prisma file reads more approachablyDrizzle sits between the Workers runtime and whichever SQL store a given 2nth build is using that week. It's the layer that makes D1, Turso, Postgres-via-Hyperdrive, and Neon all feel like the same database to the application code — so the build can start on D1 and graduate to Postgres without rewriting the data layer.
postgres-js and node-postgres drivers point at the Hyperdrive connection string unchanged.pgvector via custom column types.The Drizzle docs are the fastest way in — dialect-specific, with runnable examples for every driver. The GitHub is active, the Discord answers quickly, and the Studio is worth trying before you pick an ORM at all.