# How to Run Database Migrations with Drizzle ORM for Cloudflare D1

> Easily run database migrations with Drizzle ORM for Cloudflare D1. Generate SQL with drizzle-kit and apply using Wrangler CLI for seamless schema updates.

- Repository: [Muhammad Arifin/fullstack-next-cloudflare](https://github.com/ifindev/fullstack-next-cloudflare)
- Tags: how-to-guide
- Published: 2026-03-03

---

**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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.config.ts) file contains the production-oriented configuration. It points to the remote D1 database using your Cloudflare account ID and D1 token.

```typescript
// 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.local.config.ts) file targets the local SQLite file generated by `wrangler d1` during development.

```typescript
// 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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts), which re-exports schemas from feature modules like [`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts). Drizzle reads this file when generating migrations.

```typescript
// 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/`:

```bash
pnpm run db:generate:named "add_due_date_to_todos"

```

This executes `drizzle-kit generate` using the configuration defined in [`drizzle.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.config.ts). The output appears as a numbered file like [`src/drizzle/0001_add_due_date_to_todos.sql`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/drizzle/0001_add_due_date_to_todos.sql), following the initial [`src/drizzle/0000_initial_schemas_migration.sql`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/package.json) provides convenience scripts that wrap Wrangler CLI commands for different environments.

### Local Development

Apply migrations to your local SQLite database:

```bash
pnpm run db:migrate:local

```

This executes:

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

```

### Preview Environment

Deploy migrations to a preview deployment:

```bash
pnpm run db:migrate:preview

```

This runs:

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

```

### Production Environment

Apply migrations to the live production database:

```bash
pnpm run db:migrate:prod

```

This executes:

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

```

## Database Inspection and Management

### Drizzle Studio

Inspect your local database schema and data using **Drizzle Studio**:

```bash
pnpm run db:studio:local

```

This launches `drizzle-kit studio` using the [`drizzle.local.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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:

```bash
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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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.

```typescript
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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.config.ts) (production) and [`drizzle.local.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/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`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/package.json) simply provide convenient shortcuts that ensure the correct configuration files and environment variables are used consistently across the team.