How to Run Database Migrations with Drizzle ORM for Cloudflare D1

To run database migrations with Drizzle ORM for D1, generate SQL files using drizzle-kit and apply them via Wrangler CLI commands that target your Cloudflare D1 database binding.

The ifindev/fullstack-next-cloudflare repository implements a production-ready workflow for version-controlling SQLite schemas in Cloudflare D1 using Drizzle ORM and drizzle-kit. This architecture separates local development from production environments while maintaining type-safe database access through runtime dependency injection.

Configuration Files

The project maintains two separate Drizzle configurations to handle different environments without credential conflicts.

Production Configuration (drizzle.config.ts)

The drizzle.config.ts file contains the production-oriented configuration. It points to the remote D1 database using your Cloudflare account ID and D1 token.

// drizzle.config.ts
export default {
  schema: './src/db/schema.ts',
  out: './src/drizzle',
  dialect: 'sqlite',
  driver: 'd1-http',
  dbCredentials: {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
    databaseId: process.env.CLOUDFLARE_D1_TOKEN,
    token: process.env.CLOUDFLARE_D1_TOKEN,
  },
};

Local Development Configuration (drizzle.local.config.ts)

The drizzle.local.config.ts file targets the local SQLite file generated by wrangler d1 during development.

// drizzle.local.config.ts
export default {
  schema: './src/db/schema.ts',
  out: './src/drizzle',
  dialect: 'sqlite',
  dbCredentials: {
    url: './.wrangler/state/v3/d1/miniflare-D1DatabaseObject/.../db.sqlite',
  },
};

The Migration Workflow

Running database migrations with Drizzle ORM for D1 follows a three-step process: modify the schema, generate the SQL, and execute against the database.

Step 1: Define Your Schema in src/db/schema.ts

All table definitions are centralized in src/db/schema.ts, which re-exports schemas from feature modules like src/modules/todos/schemas/todo.schema.ts. Drizzle reads this file when generating migrations.

// src/db/schema.ts
export * from '@/modules/todos/schemas/todo.schema';
export * from '@/modules/categories/schemas/category.schema';

Step 2: Generate Migration Files with drizzle-kit

After modifying your schema, run the generation command to create a new SQL migration file in src/drizzle/:

pnpm run db:generate:named "add_due_date_to_todos"

This executes drizzle-kit generate using the configuration defined in drizzle.config.ts. The output appears as a numbered file like src/drizzle/0001_add_due_date_to_todos.sql, following the initial src/drizzle/0000_initial_schemas_migration.sql.

Step 3: Apply Migrations Using Wrangler

Use Wrangler's D1 migration runner to execute the SQL files against your database. The command reads all *.sql files in src/drizzle/ and runs them sequentially against the next-cf-app binding defined in wrangler.jsonc.

Environment-Specific Migration Commands

The package.json provides convenience scripts that wrap Wrangler CLI commands for different environments.

Local Development

Apply migrations to your local SQLite database:

pnpm run db:migrate:local

This executes:

wrangler d1 migrations apply next-cf-app --local

Preview Environment

Deploy migrations to a preview deployment:

pnpm run db:migrate:preview

This runs:

wrangler d1 migrations apply next-cf-app --env preview

Production Environment

Apply migrations to the live production database:

pnpm run db:migrate:prod

This executes:

wrangler d1 migrations apply next-cf-app --remote

Database Inspection and Management

Drizzle Studio

Inspect your local database schema and data using Drizzle Studio:

pnpm run db:studio:local

This launches drizzle-kit studio using the drizzle.local.config.ts configuration, connecting to the local .sqlite file for visual browsing and ad-hoc queries.

Resetting the Local Database

During iterative schema development, reset the local database and re-apply all migrations:

pnpm run db:reset:local

This script drops existing tables and re-runs db:migrate:local, effectively giving you a fresh database instance seeded with your current schema.

Using the Database in Application Code

The src/db/index.ts file exports a getDb() helper that creates a Drizzle instance bound to the Cloudflare D1 binding. This pattern enables dependency injection without hard-coding environment-specific connections.

import { eq } from "drizzle-orm";
import { getDb } from "@/db";
import { todos } from "@/db/schema";

export async function fetchPendingTodos(userId: string) {
  const db = await getDb();
  return db
    .select()
    .from(todos)
    .where(eq(todos.user_id, userId))
    .and(eq(todos.status, "pending"));
}

The same getDb() function works across local, preview, and production environments because it injects the D1 binding at runtime rather than initializing a global connection.

Summary

  • Configuration: Maintain separate drizzle.config.ts (production) and drizzle.local.config.ts (development) files to handle different D1 connection methods via HTTP API and local SQLite respectively.
  • Schema Management: Define tables in feature modules and re-export them from src/db/schema.ts for Drizzle to consume when generating migrations.
  • Generation: Use pnpm run db:generate:named to create versioned SQL files in src/drizzle/ after schema changes, following the pattern of 0000_initial_schemas_migration.sql.
  • Execution: Apply migrations via environment-specific scripts (db:migrate:local, db:migrate:preview, db:migrate:prod) that wrap wrangler d1 migrations apply.
  • Runtime Access: Use the getDb() helper from src/db/index.ts to inject the D1 binding into type-safe Drizzle queries without environment-specific code branches.

Frequently Asked Questions

How does Drizzle ORM connect to Cloudflare D1?

Drizzle ORM connects to D1 through the drizzle-orm/d1 driver, which accepts a D1Database binding provided by the Cloudflare Workers runtime. The getDb() function in src/db/index.ts wraps this initialization, returning a fully-typed Drizzle instance that you can use to build SQL queries. This binding is passed at runtime from the env object in your Worker or Next.js edge function.

What is the difference between drizzle.config.ts and drizzle.local.config.ts?

The drizzle.config.ts file configures drizzle-kit to connect to remote D1 databases using HTTP API credentials (account ID and API token), while drizzle.local.config.ts points to a local SQLite file created by Wrangler's miniflare simulation. This separation ensures that local development commands never accidentally modify production data, and production commands require explicit authentication.

How do I reset the database during development?

Run pnpm run db:reset:local to wipe your local D1 database and re-apply all migrations from scratch. This command executes Wrangler's D1 execute command to drop tables, then triggers db:migrate:local. Use this when you need to test migration sequences or clean up corrupted local state during iterative schema design.

Can I run migrations manually without the npm scripts?

Yes, you can invoke Wrangler CLI commands directly. For local development: wrangler d1 migrations apply next-cf-app --local. For production: wrangler d1 migrations apply next-cf-app --remote. The npm scripts in package.json simply provide convenient shortcuts that ensure the correct configuration files and environment variables are used consistently across the team.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →