know.2nth.ai Technology tech frameworks drizzle
tech/frameworks · Drizzle ORM · Skill Leaf

The TypeScript ORM that stays out of the way.

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.

Draft TypeScript Apache 2.0 Edge-ready Postgres · MySQL · SQLite · D1

A query builder with a schema-aware type system.

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.

Why this matters for 2nth builds

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.

The ORM tax is real. Drizzle doesn't charge it.

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.

ConcernPrismaDrizzle
Schema sourceCustom .prisma DSLTypeScript file
Type generationCodegen step — prisma generatePure type inference — no step
Query engineRust binary (historically); now WASM/TSNo engine — it's a query builder
Edge / WorkersSupported via driver adaptersFirst-class, from day one
Bundle sizeLarge — client + engine~7 kB min+gz
SQL controlAbstracted away; escape via $queryRawYou write SQL-shaped TypeScript
Migration modelSchema-as-source-of-truthSchema-as-source-of-truth — plus optional pull from DB

schema.ts is the unit of everything.

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.

Two APIs in one ORM

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.

More than the query builder.

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 / toolWhat it does
drizzle-ormThe query builder and type-inference core. Import the dialect-specific entry point (/pg-core, /mysql-core, /sqlite-core).
drizzle-kitCLI for migrations. generate diffs schema vs. last snapshot; migrate applies; push syncs directly (dev only); pull introspects an existing DB.
Drizzle StudioLocal browser-based data explorer. drizzle-kit studio spins it up — read/edit rows, run queries, inspect schema. Equivalent to Prisma Studio; runs offline.
Dialect driverspostgres-js, node-postgres, @neondatabase/serverless, mysql2, @planetscale/database, better-sqlite3, bun:sqlite, Cloudflare D1 binding, @libsql/client (Turso), Expo SQLite.
Relations APIDefine one/many relations alongside tables; use db.query.X.findMany({ with: ... }) for nested loads without writing joins.
Zod / Valibot / Yup adaptersdrizzle-zod generates insert/select validators from your schema. One source of truth across API boundary validation and DB layer.
drizzle-seedDeterministic seed data generator that respects your schema — relations, constraints, and all.

Where this lands in the 2nth stack.

Worker → D1 storefront cache

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.

Worker → Hyperdrive → Postgres

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.

Agent context and event stores

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.

Bun / Deno side services

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.

From "third ORM" to default for edge TypeScript.

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.

EraWhat shippedWhat it unlocked
2022Drizzle ORM v0 — SQL-like query builder for PostgresProof that TS inference could replace codegen
2023MySQL + SQLite dialects, drizzle-kit migrations, D1 adapterOne ORM for every SQL target that matters on the edge
2024Relations API, Drizzle Studio, Turso + Neon serverless driversErgonomic queries without giving up edge-first design
2025drizzle-seed, RQB v2, stronger multi-dialect schema helpersSeeds, nested queries, and schema-as-TS matured into a serious production toolkit
2026Widely adopted as the default Workers/D1 ORM; strong Bun and Expo supportStandard library status in the TS-on-edge stack

When to use it. When to skip it.

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.

Use when

  • You're writing a TypeScript service (Worker, Bun, Node, Deno) that owns its own tables
  • You target Cloudflare D1 or Turso and want a real ORM, not raw env.DB.prepare()
  • You need to move the same schema between SQLite (dev, D1) and Postgres (prod, Hyperdrive)
  • You want migrations in git, reviewable as SQL, generated from a typed schema
  • You care about cold-start size and bundle budgets on the edge
  • You want one schema feeding both DB access and API-layer validation (via drizzle-zod)

How this node connects in the tree.

Drizzle 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.

Go deeper.

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.