/ 7 min read

Drizzle Migrations on Cloudflare D1: Generate SQL, Apply with Wrangler

Database migrations are one of those things that have always piqued my interest. I started with Django migrations a long time ago, then Flyway when I used Spring Boot (I even wrote a blog post about it!) and now mostly Drizzle with Cloudflare D1.

There is a Drizzle D1 adapter but my approach does not use that. Instead we use a combination of generating SQL migrations, and running them using the wrangler CLI.

Setting Up a Cloudflare Worker with D1

If you already have a Cloudflare Worker set up with a D1 binding you can skip this step and move on to adding Drizzle.

For this we’ll create a simple React + Vite worker using the create-cloudflare CLI:

Terminal window
npm create cloudflare@latest -- my-react-app --framework=react

Then add a D1 database.

Terminal window
wrangler d1 create migrations-example-db

Then let wrangler add the database binding to wrangler.jsonc and choose DB as a binding name.

✅ Successfully created DB 'migrations-example-db' in region ENAM
Created your new D1 database.
To access your new D1 Database in your Worker, add the following snippet to your configuration file:
{
"d1_databases": [
{
"binding": "migrations_example_db",
"database_name": "migrations-example-db",
"database_id": "<the id>"
}
]
}
✔ Would you like Wrangler to add it on your behalf? … yes
✔ What binding name would you like to use? … DB
✔ For local dev, do you want to connect to the remote resource instead of a local resource? … no

Adding Drizzle ORM to a Cloudflare Worker

Now that we have a worker and D1 database, it’s time to add Drizzle.

Terminal window
npm i drizzle-orm
npm i -D drizzle-kit

Once that’s added, we create a Drizzle config:

drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle/migrations',
schema: './src/db/schema.ts',
dialect: 'sqlite',
driver: 'd1-http'
});

Notice that we do not add any D1 credentials.

As you can see, we have an out folder. Now the next crucial step is to add that in wrangler.jsonc:

wrangler.jsonc
"d1_databases": [
{
"binding": "DB",
"database_name": "migrations-example-db",
"database_id": "<the id>",
"migrations_dir": "./drizzle/migrations"
}
]

For the purpose of this article I will create the simplest database schema I can think of, a single counter.

src/db/schema.ts
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const counters = sqliteTable("counters", {
name: text("name").primaryKey(),
value: integer("value").notNull().default(0),
});

Generating and Applying D1 Migrations

Next is a simple 2 step process to run the migrations. First, you generate them. Then you run them. We do so by adding scripts to package.json:

package.json
"name": "my-react-app",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "pnpm run build && vite preview",
"deploy": "pnpm run build && wrangler deploy",
"cf-typegen": "wrangler types",
"db:generate": "drizzle-kit generate",
"db:migrate": "wrangler d1 migrations apply DB"
},

As you can see we generate migrations using drizzle-kit, but then apply them using wrangler. This is the key difference with the “pure drizzle-kit” approach.

Now we can generate migrations:

$ npm run db:generate
> my-react-app@0.0.0 db:generate /my-react-app
> drizzle-kit generate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/drizzle.config.ts'
1 tables
counters 2 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/migrations/0000_spicy_johnny_blaze.sql 🚀

And apply them:

$ npm run db:migrate
> my-react-app@0.0.0 db:migrate /my-react-app
> wrangler d1 migrations apply DB
⛅️ wrangler 4.78.0
───────────────────
Resource location: local
Use --remote if you want to access the remote instance.
Migrations to be applied:
┌─────────────────────────────┐
│ name │
├─────────────────────────────┤
│ 0000_spicy_johnny_blaze.sql │
└─────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database DB (<the id> from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
┌─────────────────────────────┬────────┐
│ name │ status │
├─────────────────────────────┼────────┤
│ 0000_spicy_johnny_blaze.sql │ ✅ │
└─────────────────────────────┴────────┘

If you want to apply them remotely, you add --remote.

Using Drizzle ORM with D1 in a Worker

Now let’s update the default worker to actually use Drizzle ORM:

worker/index.ts
import { drizzle } from "drizzle-orm/d1";
import { sql } from "drizzle-orm";
import { counters } from "../src/db/schema";
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) {
if (url.pathname === "/api/counter") {
const db = drizzle(env.DB);
const [row] = await db
.insert(counters)
.values({ name: "visits", value: 1 })
.onConflictDoUpdate({
target: counters.name,
set: { value: sql`${counters.value} + 1` },
})
.returning();
return Response.json({ count: row.value });
}
return new Response(null, { status: 404 });
},

Now if you make a request to /api/counter, it will increase “visits” by 1.

Drizzle 1.0.0 Migration Format Changes

At the time of writing, Drizzle 1.0.0 is in beta. However, they changed the structure of migrations. If you upgrade to 1.0.0 you now get:

$ npm run db:generate
> my-react-app@0.0.0 db:generate /my-react-app
> drizzle-kit generate
No config path provided, using default 'drizzle.config.ts'
Reading config file '/drizzle.config.ts'
[✓] Your SQL migration ➜ drizzle/migrations/20260329172732_old_marvel_apes/migration.sql 🚀

This is great because it removes that huge journal.json file you might have seen, but wrangler doesn’t run migrations in subfolders. It just applies migrations in order.

Luckily we can create an incredibly simple Node script that flattens the migrations and sorts them in the right order.

For this I created scripts/sync-d1-migrations.mjs:

scripts/sync-d1-migrations.mjs
import { cp, mkdir, readdir, rm } from 'node:fs/promises';
const src = 'drizzle/migrations';
const dest = 'drizzle/d1';
await rm(dest, { recursive: true, force: true });
await mkdir(dest, { recursive: true });
const dirs = (await readdir(src, { withFileTypes: true }))
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
await Promise.all(
dirs.map((dir, i) => {
const name = dir.split('_').slice(1).join('_') || dir;
return cp(`${src}/${dir}/migration.sql`, `${dest}/${String(i).padStart(4, '0')}_${name}.sql`);
}),
);
console.log(`Synced ${dirs.length} migration${dirs.length === 1 ? '' : 's'} to ${dest}`);

Then I add a new script and prepend it to the migration script:

package.json
"name": "my-react-app",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "pnpm run build && vite preview",
"deploy": "pnpm run build && wrangler deploy",
"cf-typegen": "wrangler types",
"db:generate": "drizzle-kit generate",
"db:generate": "drizzle-kit generate && pnpm run db:sync",
"db:sync": "node scripts/sync-d1-migrations.mjs",
"db:migrate": "wrangler d1 migrations apply DB"
},

Now the same steps apply:

$ npm run db:generate
> my-react-app@0.0.0 db:generate /my-react-app
> drizzle-kit generate && npm run db:sync
No config path provided, using default 'drizzle.config.ts'
Reading config file '/drizzle.config.ts'
[✓] Your SQL migration ➜ drizzle/migrations/20260329174255_fat_microbe/migration.sql 🚀
> my-react-app@0.0.0 db:sync /my-react-app
> node scripts/sync-d1-migrations.mjs
Synced 1 migration to drizzle/d1
$ npm run db:migrate
> my-react-app@0.0.0 db:migrate /my-react-app
> wrangler d1 migrations apply DB
⛅️ wrangler 4.78.0
───────────────────
Resource location: local
Use --remote if you want to access the remote instance.
Migrations to be applied:
┌──────────────────────┐
│ name │
├──────────────────────┤
│ 0000_fat_microbe.sql │
└──────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database DB (<the id>) from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
┌──────────────────────┬────────┐
│ name │ status │
├──────────────────────┼────────┤
│ 0000_fat_microbe.sql │ ✅ │
└──────────────────────┴────────┘

Conclusion

You (and your coding agent) should now be able to add Drizzle to any Cloudflare Worker project and run migrations. As a bonus, migrations are checked into version control as SQL files and applied by wrangler.

In production environments, you would do this as part of a build step (e.g. pre-deploy with changes to drizzle/*).