Updated June 2026Cookbook18 min read

Drizzle skill: 10 schema + query patterns you can ship

Ten real Drizzle ORM patterns — a schema-first pgTable, a reviewed drizzle-kit migration, typed joins, a junction table, a drizzle-zod validator, soft-delete, a safe upsert, a recursive CTE, and a dialect port — each as a single Claude prompt with the exact Drizzle code it produces.

One thing to set expectations: this skill is LobeHub’s house style, not a neutral Drizzle primer. It has strong opinions — text IDs over serial, the db.select() builder over the relational API, JSONB typed against a real interface. That’s the point. A style-guide skill is most useful precisely because it stops you re-litigating the same decisions on every table.

Already know what skills are? Skip to the cookbook. First time? Read the explainer then come back. Need the install? It’s on the /skills/drizzle page.

Editorial illustration: a TypeScript schema file on the left flowing into a relational table diagram on the right, connected by a luminous teal arc, on a midnight navy background.
On this page · 21 sections
  1. What this skill does
  2. The cookbook
  3. Install + README
  4. Watch it built
  5. 01 · Schema-first table from a plain-English spec
  6. 02 · Generate and apply a drizzle-kit migration
  7. 03 · Typed JOIN with the select builder (no relational API)
  8. 04 · Aggregation query with groupBy and a composite index
  9. 05 · Junction table for a many-to-many
  10. 06 · drizzle-zod schema for API input validation
  11. 07 · Soft-delete + updatedAt pattern
  12. 08 · Upsert with onConflictDoUpdate (no COALESCE soup)
  13. 09 · Recursive CTE that stays raw (the right exception)
  14. 10 · Seed script with fake data
  15. Community signal
  16. The contrarian take
  17. Real data layers
  18. Gotchas
  19. Pairs well with
  20. FAQ
  21. Sources

What this skill actually does

Sixty seconds of context before the cookbook — what the Drizzle skill shapes, what Claude returns when you invoke it, and the one thing it does NOT do for you.

What this skill actually does

Drizzle ORM schema and database guide. Use when working with database schemas, defining tables, creating migrations, or database model code.

lobehub, the skill author · /skills/drizzle

What Claude returns

When triggered, Claude writes Drizzle code in LobeHub's house style: `pgTable` schemas with text or uuid primary keys (never `serial`), snake_case columns, JSONB columns typed against a concrete interface, timestamps spread from a `_helpers.ts`, and indexes returned as an array from the table callback. Queries come back through the `db.select()` builder with explicit joins, plus `$inferInsert`/`$inferSelect` types and a `createInsertSchema` validator. It points migration work at `drizzle-kit`.

What it does NOT do

It does not install Drizzle or run your database. You still `npm i drizzle-orm drizzle-kit`, wire `drizzle.config.ts`, and apply migrations yourself.

How you trigger it

Add a Drizzle pgTable for orders with a userId FK.Write a typed leftJoin query for this repository method.Generate a drizzle-kit migration for the column I just added.

Cost when idle

~100 tokens at idle (the skill name + description in the system prompt). The full style guide and query patterns load only when a database task triggers it.

The cookbook

Each entry is a Drizzle task you could ship today. They run roughly from schema outward — the early ones define tables and migrations, the middle ones cover the query builder the skill insists on, and the later ones handle the awkward cases (soft-delete, upserts, recursive CTEs, dialect ports). Every entry pairs with a skill or MCP server you already have on mcp.directory.

The code in each snippet follows the skill’s real conventions, lifted from LobeHub’s own schema guide: idGenerator IDs, snake_case columns, the ...timestamps spread, and indexes returned as an array. Swap those helper names for yours and the shape carries over to any Drizzle project.

Install + README

If the skill isn’t on your machine yet, here’s the one-liner. The full install panel (Claude Code, Codex, Copilot, Antigravity variants) is on the skill page — the same UI’s embedded below.

One-line install · by lobehub

Open skill page

Install

mkdir -p .claude/skills/drizzle && curl -L -o skill.zip "https://mcp.directory/api/skills/download/859" && unzip -o skill.zip -d .claude/skills/drizzle && rm skill.zip

Installs to .claude/skills/drizzle

Watch it built

A full beginner course on Drizzle ORM — schema definition, the query builder, and migrations. Useful before the cookbook because it anchors the API surface (pgTable, db.select(), drizzle-kit) every prompt below picks up.

01

Schema-first table from a plain-English spec

Turn a one-paragraph description of an entity into a `pgTable` definition that matches house style: a text primary key with a generated default, snake_case columns, typed JSONB, and the timestamp spread.

ForAnyone starting a new table and tired of re-typing the same boilerplate.

The prompt

Add a Drizzle `pgTable` for `agents` to `src/database/schemas/agent.ts`. Columns: a text `id` primary key defaulted with `idGenerator('agents')`, a `userId` FK to `users.id` with `onDelete: 'cascade'`, a `slug` varchar(100) unique, a `chatConfig` JSONB column typed against a `LobeAgentChatConfig` interface, and the standard created/updated/accessed timestamps via the `...timestamps` spread from `_helpers.ts`. Add a unique index on `(clientId, userId)`. Export `$inferInsert` and `$inferSelect` types.

What slides.md looks like

export const agents = pgTable(
  'agents',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => idGenerator('agents'))
      .notNull(),
    userId: text('user_id')
      .references(() => users.id, { onDelete: 'cascade' })
      .notNull(),
    slug: varchar('slug', { length: 100 }).unique(),
    chatConfig: jsonb('chat_config').$type<LobeAgentChatConfig>(),
    ...timestamps,
  },
  (t) => [uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId)],
);

export type NewAgent = typeof agents.$inferInsert;
export type AgentItem = typeof agents.$inferSelect;

One-line tweak

Swap `text('id').$defaultFn(...)` for `uuid('id').defaultRandom()` and the same prompt scaffolds an internal table instead of a public, prefix-ID one.

02

Generate and apply a drizzle-kit migration

After editing the schema, produce the SQL migration with `drizzle-kit generate`, eyeball the diff, then apply it. The skill keeps migrations as plain SQL files you can read and hand-edit.

ForDevelopers who want migrations in git, not hidden behind a runtime engine.

The prompt

I just added a `lastSeenAt` timestamptz column to the `users` schema. Generate the Drizzle migration with `drizzle-kit generate` (config at `drizzle.config.ts`, dialect postgresql, strict true), show me the generated SQL before anything touches the database, and only then run `drizzle-kit migrate`. If the generated file does a destructive column rename instead of an add, stop and tell me — I want to hand-edit it into an additive migration with a backfill.

What slides.md looks like

# drizzle.config.ts already points at the schema + migrations dir
npx drizzle-kit generate
# → packages/database/migrations/0007_curly_magneto.sql

-- 0007_curly_magneto.sql (review before applying)
ALTER TABLE "users" ADD COLUMN "last_seen_at" timestamp with time zone;

npx drizzle-kit migrate   # applies pending migrations in order

One-line tweak

Add `--name add_last_seen` to the generate call so the migration file gets a readable slug instead of the random two-word codename.

03

Typed JOIN with the select builder (no relational API)

Fetch a parent row plus joined columns using `db.select()` with explicit `leftJoin`. The skill deliberately avoids `db.query.*` with `with:` because the relational API emits fragile lateral joins.

ForTeams that want their joins to read like the SQL they compile to.

The prompt

Write a repository method `getRunTopics(runId)` that returns eval-run topics joined to their test cases and topics. Use the `db.select()` builder with explicit `leftJoin` — do NOT use `db.query.agentEvalRunTopics.findMany` or `with:`, our house style forbids the relational API. Select `runId`, `score`, the full `testCase` row, and the full `topic` row. Filter by `runId`, order by `createdAt` ascending.

What slides.md looks like

const rows = await this.db
  .select({
    runId: agentEvalRunTopics.runId,
    score: agentEvalRunTopics.score,
    testCase: agentEvalTestCases,
    topic: topics,
  })
  .from(agentEvalRunTopics)
  .leftJoin(
    agentEvalTestCases,
    eq(agentEvalRunTopics.testCaseId, agentEvalTestCases.id),
  )
  .leftJoin(topics, eq(agentEvalRunTopics.topicId, topics.id))
  .where(eq(agentEvalRunTopics.runId, runId))
  .orderBy(asc(agentEvalRunTopics.createdAt));

One-line tweak

Need a parent with its children instead of a flat join? Ask for two simple queries — one for the parent, one `where(eq(...))` for the children — which is the skill's one-to-many pattern.

04

Aggregation query with groupBy and a composite index

Count children per parent with `leftJoin` + `groupBy`, then add the composite index that makes the filter fast. The skill returns indexes as an array from the table callback (object style is deprecated).

ForAnyone building a dashboard count that's about to hit a sequential scan.

The prompt

Write a method that returns each eval dataset with its test-case count. Use `db.select()` with `count(agentEvalTestCases.id).as('testCaseCount')`, a `leftJoin` on `datasetId`, and `groupBy(agentEvalDatasets.id)`. Then add a composite index on `agentEvalTestCases` over `(datasetId, sortOrder)` so the join and ordering are index-backed. Return the index as an array entry from the table's second callback argument.

What slides.md looks like

const rows = await this.db
  .select({
    id: agentEvalDatasets.id,
    name: agentEvalDatasets.name,
    testCaseCount: count(agentEvalTestCases.id).as('testCaseCount'),
  })
  .from(agentEvalDatasets)
  .leftJoin(
    agentEvalTestCases,
    eq(agentEvalDatasets.id, agentEvalTestCases.datasetId),
  )
  .groupBy(agentEvalDatasets.id);

// in the schema table callback:
(t) => [index('test_cases_dataset_sort_idx').on(t.datasetId, t.sortOrder)],

One-line tweak

Swap `count()` for an expression-level `sql<number>` average inside the same select object — raw SQL at the expression level is allowed where no builder helper exists.

05

Junction table for a many-to-many

Model a many-to-many with a junction `pgTable` that carries `userId` for row-level scoping and a composite primary key over both foreign keys. Both FKs cascade on delete.

ForAnyone wiring agents to knowledge bases, users to teams, posts to tags.

The prompt

Create a junction table `agents_knowledge_bases` linking `agents` and `knowledgeBases`. Columns: `agentId` and `knowledgeBaseId` as text FKs (both `onDelete: 'cascade'`), a `userId` text FK for row scoping (also cascade), an `enabled` boolean defaulting true, and the `...timestamps` spread. Make the primary key composite over `(agentId, knowledgeBaseId)` using `primaryKey({ columns: [...] })` returned from the table callback.

What slides.md looks like

export const agentsKnowledgeBases = pgTable(
  'agents_knowledge_bases',
  {
    agentId: text('agent_id')
      .references(() => agents.id, { onDelete: 'cascade' })
      .notNull(),
    knowledgeBaseId: text('knowledge_base_id')
      .references(() => knowledgeBases.id, { onDelete: 'cascade' })
      .notNull(),
    userId: text('user_id')
      .references(() => users.id, { onDelete: 'cascade' })
      .notNull(),
    enabled: boolean('enabled').default(true),
    ...timestamps,
  },
  (t) => [primaryKey({ columns: [t.agentId, t.knowledgeBaseId] })],
);

One-line tweak

Add a `role` varchar column to the junction table and you've turned a plain membership link into a permissions edge — same composite key, richer relationship.

06

drizzle-zod schema for API input validation

Derive a Zod insert schema straight from the table with `createInsertSchema`, so the API validator and the database never drift. One source of truth, two consumers.

ForAPI developers who keep a separate hand-written Zod schema in sync by hand.

The prompt

From the `agents` table, derive a drizzle-zod insert schema with `createInsertSchema(agents)`, then narrow it for the public create endpoint: omit `id`, `userId`, and the timestamp columns (the server sets those), and require `slug` to be a 3–100 char slug. Export the narrowed schema as `createAgentInput` and infer its TypeScript type. Show me how to parse `req.body` with it in the route handler.

What slides.md looks like

import { createInsertSchema } from 'drizzle-zod';
import { z } from 'zod';

export const insertAgentSchema = createInsertSchema(agents);

export const createAgentInput = insertAgentSchema
  .omit({ id: true, userId: true, createdAt: true, updatedAt: true, accessedAt: true })
  .extend({ slug: z.string().min(3).max(100) });

export type CreateAgentInput = z.infer<typeof createAgentInput>;

// route handler
const input = createAgentInput.parse(req.body);

One-line tweak

Swap `createInsertSchema` for `createSelectSchema` to generate the response validator instead — useful for typing what you send back over the wire.

07

Soft-delete + updatedAt pattern

Add a nullable `deletedAt` timestamptz, keep `updatedAt` fresh on writes, and make queries filter out tombstoned rows by default. The skill favours nullable columns over sentinel strings for absent state.

ForApps that need an undo / audit trail instead of hard deletes.

The prompt

Add soft-delete to the `agents` table: a nullable `deletedAt` timestamptz column (absent means live). Write a `softDelete(id)` repository method that sets `deletedAt` and bumps `updatedAt` to now instead of issuing a DELETE. Write a `listActive(userId)` method that filters `isNull(agents.deletedAt)` and `eq(agents.userId, userId)`. Do not invent a status string like 'deleted' — use the nullable timestamp as the source of truth.

What slides.md looks like

// schema
deletedAt: timestamptz('deleted_at'),  // null = live row

// soft delete instead of DELETE
await this.db
  .update(agents)
  .set({ deletedAt: new Date(), updatedAt: new Date() })
  .where(eq(agents.id, id));

// only-live read
const rows = await this.db
  .select()
  .from(agents)
  .where(and(eq(agents.userId, userId), isNull(agents.deletedAt)));

One-line tweak

Add a partial index `where deleted_at is null` so the live-row query stays cheap even when the tombstone count grows large.

08

Upsert with onConflictDoUpdate (no COALESCE soup)

Write an insert-or-update that only sets the scalars you actually have, instead of scattering `COALESCE(excluded.col, current.col)` across every field. The skill builds the `set` object from defined values only.

ForAnyone whose upsert turned into a wall of SQL plumbing.

The prompt

Write an upsert for `userSignupLogs` keyed on `id`. Build the update `set` only from values that are actually present — use a `compactUndefined` helper to drop undefined scalars rather than wrapping every column in COALESCE. For the JSONB `stageResults` column, append the new stage via a named helper `appendStageResult(stage, result)` so the method reads as business intent. Always bump `updatedAt`.

What slides.md looks like

const updateValues = compactUndefined({
  email: record.email ?? undefined,
  ip: record.ip ?? undefined,
});

await db
  .insert(userSignupLogs)
  .values(values)
  .onConflictDoUpdate({
    target: userSignupLogs.id,
    set: {
      ...updateValues,
      stageResults: appendStageResult(stage, result),
      updatedAt: new Date(),
    },
  });

One-line tweak

Need an insert-only path? Swap `onConflictDoUpdate` for `onConflictDoNothing()` to make the write idempotent without touching the existing row.

09

Recursive CTE that stays raw (the right exception)

Some queries shouldn't be forced through the builder. A `WITH RECURSIVE` tree walk is the canonical keep-raw case — the skill keeps it in `db.execute<T>(sql...)` but tightens it with schema refs and user scoping.

ForDevelopers building comment trees, task hierarchies, org charts.

The prompt

Write a method `getTaskTree(rootTaskId, userId)` that walks a self-referencing `tasks` table with a recursive CTE. Keep it as raw `db.execute<TaskTreeRow>(sql...)` — there's no clean WITH RECURSIVE builder and a rewrite would add depth-based roundtrips. But interpolate the schema column refs (`tasks.id`, `tasks.parentTaskId`) instead of string literals, scope every leg of the recursion to `createdByUserId = userId`, and type the rows with a narrow `TaskTreeRow` interface.

What slides.md looks like

interface TaskTreeRow {
  id: string;
  parent_task_id: string | null;
}

const { rows } = await db.execute<TaskTreeRow>(sql`
  WITH RECURSIVE task_tree AS (
    SELECT ${tasks.id}, ${tasks.parentTaskId}
    FROM ${tasks}
    WHERE ${tasks.id} = ${rootTaskId}
      AND ${tasks.createdByUserId} = ${userId}
    UNION ALL
    SELECT ${tasks.id}, ${tasks.parentTaskId}
    FROM ${tasks}
    JOIN task_tree ON ${tasks.parentTaskId} = task_tree.id
    WHERE ${tasks.createdByUserId} = ${userId}
  )
  SELECT * FROM task_tree
`);

One-line tweak

Add a `depth` column to the CTE (`SELECT ..., 0` in the anchor, `task_tree.depth + 1` in the recursive leg) to cap traversal or render indentation.

10

Seed script with fake data

Generate a standalone seed script that inserts a few hundred realistic rows with `db.insert(...).values([...])`, respecting FK order (parents before children) and the table's `$inferInsert` type.

ForAnyone who needs a populated local database to demo or test against.

The prompt

Write a `scripts/seed.ts` that seeds the local database: 20 users, then 100 agents distributed across those users, then 300 agent/knowledge-base junction rows. Use `@faker-js/faker` for names and slugs. Type each batch with the table's `$inferInsert` so a schema change breaks the seed at compile time. Insert parents before children to satisfy the foreign keys, and wrap the whole thing in a single transaction so a failure rolls back cleanly.

What slides.md looks like

import { faker } from '@faker-js/faker';

await db.transaction(async (tx) => {
  const userRows: (typeof users.$inferInsert)[] = Array.from(
    { length: 20 },
    () => ({ id: idGenerator('users'), email: faker.internet.email() }),
  );
  await tx.insert(users).values(userRows);

  const agentRows: (typeof agents.$inferInsert)[] = userRows.flatMap((u) =>
    Array.from({ length: 5 }, () => ({
      id: idGenerator('agents'),
      userId: u.id!,
      slug: faker.helpers.slugify(faker.word.words(2)),
    })),
  );
  await tx.insert(agents).values(agentRows);
});

One-line tweak

Port the same script to a different dialect by importing `drizzle` from `drizzle-orm/libsql` (Turso) or `drizzle-orm/d1` (Cloudflare) — the schema and inserts stay identical, only the connector changes.

Community signal

Three voices from developers running Drizzle in production. The first is the migrations-as-SQL argument, the second is the native-TypeScript endorsement, the third is the “you already know the query language” case against DSL-based ORMs.

migrations are just sql and thus easily edited to handle complex data migrations... no runtime engine, it is just a thin wrapper on sql.

u/Insensibilities · Reddit

Top comment in 'What's the best nodejs ORM in 2026?' — the migrations-as-SQL argument that use cases 2 and 9 lean on.

Source
Native TS integration - The migrations system is wonderful - The API is more intuitive, imo.

nullable_bool · Hacker News

Hacker News, on the 'Drizzle Joins PlanetScale' thread — comparing Drizzle to Sequelize after personal-project use.

Source
If you already know SQL Drizzle is definitely the go to. With prisma you'll need to learn additional language just to query your db.

u/abdimussa87 · Reddit

r/nextjs — the 'you already know the query language' case for picking Drizzle over a DSL-based ORM.

Source

The contrarian take

Not everyone is sold. The most honest recurring critique is about drizzle-kit, not the ORM itself — from u/Hung_Hoang_the on r/node:

i used drizzle on a side project too and liked how close to sql it felt but the migration tooling was rougher than prisma's — ran into that same silent failure thing you mentioned.

u/Hung_Hoang_the · Reddit

r/node — migration tooling rougher than Prisma's, including silent failures.

Source

Fair, and it's the gotcha this cookbook designs around. Use case 2 never auto-applies — it generates the SQL, makes you read the diff, and only then runs migrate. drizzle-kit's rough edges bite hardest when you let it apply blind; treat the generated file as a code-review artifact and most of the surprise goes away.

There’s a second critique worth naming: some argue Drizzle is so close to SQL you may as well write SQL. That’s a real position — one r/reactjs commenter put it as “you may as well just swap over to SQL”. The skill’s answer is the part raw SQL can’t give you: schema-bound references. When a column rename breaks a join, you get a TypeScript error at author time, not a runtime failure in production. That type tether is the whole reason to stay in the builder.

One comparison on intent: there are database MCP servers that complement this skill rather than replace it — Postgres MCP Pro, Postgres, and managed-database MCPs like Neon and Turso. The trade-off is the usual skill-vs-MCP one: the skill is ~100 idle tokens and shapes the code you write; an MCP’s tool schemas load every turn and let the model run queries against a live database. Use the skill to author, reach for a database MCP the moment you need the model to actually touch the data.

Real data layers built on Drizzle

Concrete examples from public projects. Most don’t use the LobeHub skill specifically — they’re here to show what a production-grade Drizzle data layer looks like, so you have a target shape in mind when you write the prompt. The first one literally is the codebase this skill was extracted from.

Gotchas (the four that bite)

Sourced from the Drizzle docs and the conventions baked into the LobeHub Drizzle skill.

Migrations don't auto-apply — and shouldn't

drizzle-kit generate writes the SQL; drizzle-kit migrate applies it. If you wire generate into a build step and never read the diff, a column rename can land as a drop-and-recreate that loses data. Review the generated file like a code change (use case 2).

The relational API is off the table here

The skill bans db.query.* with `with:` because it emits json_build_array lateral joins that are fragile to debug. Every read goes through db.select() with explicit joins. That's a house rule — Drizzle still ships the relational API; this skill just won't use it.

Type inference slows on very large schemas

On schemas with hundreds of tables, TypeScript IntelliSense can lag — one developer reported 10-second waits at 200+ tables. Split the schema into modules and keep the hottest tables lean; the type tether is worth it, but it isn't free at scale.

Indexes are an array now, not an object

The object-style index signature is deprecated. Return indexes as an array from the table's second callback: `(t) => [uniqueIndex('...').on(t.a, t.b)]`. Older tutorials still show the object form — the skill writes the array form.

Pairs well with

Curated to match the cookbook’s actual integrations: the TypeScript and framework skills the schema feeds (typescript, nextjs-developer, tanstack-query, cloudflare-d1) plus the Postgres-side MCP servers the migration, join, and upsert use cases lean on.

Two posts that compose well with this cookbook: What are Claude Code skills? covers the underlying mechanism, and What is MCP? explains the server side — useful for deciding when a database MCP earns its place next to this skill.

Frequently asked questions

What is the Drizzle skill, and what does it actually do?

It's a style guide that teaches Claude how LobeHub writes Drizzle ORM code. When you ask for a table, a query, or a migration, the skill steers the output toward consistent conventions: text or uuid primary keys instead of serial, snake_case columns, JSONB typed against a real interface, timestamps from a shared helper, and the `db.select()` builder over the relational API. It's opinionated on purpose — that's the value over a blank prompt.

Drizzle skill vs the Drizzle MCP server — which should I use?

Different jobs. The Drizzle skill shapes how Claude writes schema and query code at authoring time, costing ~100 tokens at idle. A Drizzle MCP (or a Postgres MCP like `postgres-mcp-pro`) gives the model live tools to run queries, inspect a real schema, or EXPLAIN a plan against a connected database — its tool schemas load every turn. Most people want the skill for writing code and a database MCP for the few moments they need the model to touch a live database. They compose; you don't pick one.

Does the Drizzle skill work in Claude Code, Cursor, and Codex?

Yes. Agent skills are plain folders with a SKILL.md, so any agent that reads `.claude/skills` (or its own equivalent) picks them up — Claude Code, Cursor, Codex, and Copilot all do. The install panel below has a tab per client. The skill content is the same; only the install path differs.

Why does the skill forbid the Drizzle relational query API (db.query.*with:)?

Because LobeHub found that the relational API generates complex lateral joins with `json_build_array` that are fragile and hard to debug at scale. The skill standardizes on `db.select()` with explicit `leftJoin` so every query reads like the SQL it compiles to, and a schema change surfaces as a TypeScript error on the join condition. If you prefer `findMany` with `with:`, this particular skill isn't for you — that's a deliberate house rule, not a Drizzle limitation.

Will the Drizzle skill handle migrations, or just schema code?

It writes the schema and points migrations at `drizzle-kit`. The skill's own guidance keeps migrations as plain SQL files you generate with `drizzle-kit generate`, review, and apply with `drizzle-kit migrate` — see use case 2. For the deeper migration playbook, the underlying repo references a separate `db-migrations` skill; this one stops at generating and applying.

Is this Drizzle skill tied to LobeHub's codebase, or can I use it on any project?

The conventions come from LobeHub's monorepo — references to `idGenerator`, `_helpers.ts`, and a `packages/database/` layout are theirs. The patterns transfer cleanly (text IDs, typed JSONB, the select builder, drizzle-zod), but you'll swap their helper names for your own. Treat it as a strong default to adapt, not a drop-in config for a fresh repo.

Does the Drizzle skill support SQLite, Turso, and Cloudflare D1, or only Postgres?

The skill's examples are Postgres-first (`pgTable`, `timestamptz`, JSONB, strict postgresql dialect). Drizzle itself runs the same schema across SQLite, Turso (libsql), and Cloudflare D1 — use case 10 shows the dialect port, where only the connector import changes. If you're on D1 specifically, the `cloudflare-d1` skill pairs well for the binding and wrangler side.

Sources

Primary

Community

Critical and contrarian

Internal

Keep reading