# How to Manage Database Schema Changes and Migrations in Production with Drizzle ORM and Cloudflare D1

> Safely manage database schema changes and migrations in production using Drizzle ORM and Cloudflare D1. Automate SQL file generation and application in your CI/CD for seamless updates.

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

---

**You can manage database schema changes and migrations in production by combining Drizzle ORM with Cloudflare D1, using a version-controlled workflow that generates incremental SQL files locally and applies them automatically via Wrangler in your CI/CD pipeline.**

Managing database schema changes and migrations in production requires a deterministic approach to prevent data loss and ensure consistency across environments. The ifindev/fullstack-next-cloudflare repository demonstrates a production-ready strategy using Drizzle ORM to define schemas in TypeScript and Cloudflare D1 to execute versioned migrations. This setup ensures every schema change is type-safe, auditable, and deployable through automated pipelines.

## The Six-Step Production Migration Workflow

The repository implements a deterministic process for evolving your database schema safely from development to production:

1. **Update TypeScript Schema Definitions** – Modify the `sqliteTable` definitions in your module schema files (such as [`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts)). The central export in [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts) aggregates all tables, providing a single source of truth for the Drizzle ORM toolkit.

2. **Generate SQL Migration Files** – Run `pnpm run db:generate` to invoke drizzle-kit, which compares your current schema against the database state and creates an incremental SQL file under `src/drizzle/`. For descriptive naming, use `pnpm run db:generate:named "add_user_preferences"`.

3. **Validate Changes Locally** – Execute `pnpm run db:migrate:local` to apply pending migrations to your local D1 database file (`.wrangler/*.sqlite`). The local configuration in [`drizzle.local.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.local.config.ts) directs this operation to your development environment.

4. **Test Functionality** – Launch the application with `pnpm dev` or `pnpm dev:cf` and verify that the schema changes work correctly through the UI or API endpoints.

5. **Deploy via CI/CD** – Push your changes to trigger the GitHub Actions workflow in [`.github/workflows/deploy.yml`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/.github/workflows/deploy.yml). The pipeline executes `pnpm run db:migrate:prod`, which runs `wrangler d1 migrations apply next-cf-app --remote` to apply only pending migrations to the production database.

6. **Automated Schema Synchronization** – The deployment scripts ensure `db:migrate:prod` runs before the Worker upload, guaranteeing the production schema matches the application code before traffic hits the new version.

## Local Development and Testing Configuration

Local development uses a separate configuration to avoid impacting production data. The [`drizzle.local.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.local.config.ts) file resolves the local SQLite database path, typically pointing to `.wrangler/*.sqlite`.

When you run `pnpm run db:migrate:local`, Wrangler applies migrations to this local file, allowing rapid iteration without cloud resources. This local-first approach ensures you catch schema errors before they reach production.

## Production Deployment Architecture

In production, [`drizzle.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.config.ts) contains the credentials and connection details for the remote Cloudflare D1 instance. The `db:migrate:prod` script executes:

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

```

This command tracks applied migration IDs directly within the D1 database, ensuring idempotent execution. If the CI/CD pipeline runs multiple times, only new migrations execute, preventing duplicate schema changes or partial application states.

## Practical Example: Adding a Column to the Todos Table

Consider adding a `priority_level` column to track task urgency in the existing todos table.

### Step 1: Modify the Schema

Edit [`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts) to include the new column:

```typescript
// src/modules/todos/schemas/todo.schema.ts
export const todos = sqliteTable("todos", {
  // … existing columns …
  priority: text("priority")
    .$type<TodoPriorityType>()
    .notNull()
    .default(TodoPriority.MEDIUM),

  // NEW column
  priorityLevel: integer("priority_level")
    .notNull()
    .default(0),   // 0 = low, 1 = medium, 2 = high
});

```

### Step 2: Generate the Migration

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

```

This creates [`src/drizzle/0001_add_priority_level_to_todos.sql`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/drizzle/0001_add_priority_level_to_todos.sql) containing:

```sql
ALTER TABLE `todos` ADD `priority_level` integer NOT NULL DEFAULT 0;

```

### Step 3: Test Locally

```bash
pnpm run db:migrate:local
pnpm run dev

```

Verify the column appears in your local database and functions correctly with the application logic.

### Step 4: Deploy to Production

```bash
git add .
git commit -m "feat: add priority_level to todos"
git push origin main

```

The CI/CD pipeline automatically runs `pnpm run db:migrate:prod`, applying the `ALTER TABLE` statement to the production D1 database before deploying the Worker code.

## Key Files in the Migration System

Understanding these critical files helps you debug and extend the migration workflow:

- **[`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts)** – Central barrel file that re-exports all table definitions for Drizzle ORM consumption.
- **[`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts)** – Example module-specific schema demonstrating `sqliteTable` definitions.
- **[`src/drizzle/0000_initial_schemas_migration.sql`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/drizzle/0000_initial_schemas_migration.sql)** – Baseline migration showing the SQL format expected by the system.
- **[`drizzle.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.config.ts)** – Production configuration connecting to the remote Cloudflare D1 database.
- **[`drizzle.local.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.local.config.ts)** – Development configuration targeting the local `.wrangler/*.sqlite` file.
- **[`package.json`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/package.json)** – Defines automation scripts including `db:generate`, `db:migrate:local`, and `db:migrate:prod`.
- **[`.github/workflows/deploy.yml`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/.github/workflows/deploy.yml)** – CI/CD pipeline ensuring migrations run before Worker deployment.

## Summary

Managing database schema changes and migrations in production with this stack relies on deterministic, version-controlled SQL files and environment-specific configurations. Key takeaways include:

- Modify TypeScript schemas in module files (e.g., [`src/modules/todos/schemas/todo.schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/modules/todos/schemas/todo.schema.ts)) and export them via [`src/db/schema.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/src/db/schema.ts) for type safety.
- Generate migrations using `pnpm run db:generate` to create auditable SQL files in `src/drizzle/`.
- Test changes locally with `pnpm run db:migrate:local` using the configuration in [`drizzle.local.config.ts`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/drizzle.local.config.ts).
- Deploy to production via CI/CD executing `wrangler d1 migrations apply next-cf-app --remote` through the `db:migrate:prod` script.
- Rely on D1's built-in migration tracking to ensure idempotent, ordered schema updates across all environments.

## Frequently Asked Questions

### How do I generate a named migration for a specific schema change?

Run `pnpm run db:generate:named "descriptive_name"` after modifying your TypeScript schema. This creates a timestamped SQL file in `src/drizzle/` with a human-readable name, making it easier to identify the purpose of each migration in version control history.

### What happens if a production migration fails during CI/CD?

The `wrangler d1 migrations apply` command runs each migration in a transaction. If a statement fails, the transaction rolls back, leaving the database in its previous state. The CI/CD pipeline will fail before deploying the Worker, preventing application code from running against an incomplete schema.

### Can I run migrations against a preview or staging environment?

Yes. You can configure additional scripts in [`package.json`](https://github.com/ifindev/fullstack-next-cloudflare/blob/main/package.json) that target specific D1 databases using Wrangler's `--env` flag. By creating separate configurations for each environment, you can apply the same migration SQL files to preview databases before executing them in production.

### How does the system prevent the same migration from running twice?

Cloudflare D1 maintains an internal tracking table that records applied migration filenames. When you run `wrangler d1 migrations apply`, the system compares the SQL files in your `src/drizzle/` directory against this record, executing only those not yet applied. This idempotent approach ensures safe re-runs of deployment scripts without duplicate schema changes.