How to Manage Database Schema Changes and Migrations in Production with Drizzle ORM and Cloudflare D1
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:
-
Update TypeScript Schema Definitions – Modify the
sqliteTabledefinitions in your module schema files (such assrc/modules/todos/schemas/todo.schema.ts). The central export insrc/db/schema.tsaggregates all tables, providing a single source of truth for the Drizzle ORM toolkit. -
Generate SQL Migration Files – Run
pnpm run db:generateto invoke drizzle-kit, which compares your current schema against the database state and creates an incremental SQL file undersrc/drizzle/. For descriptive naming, usepnpm run db:generate:named "add_user_preferences". -
Validate Changes Locally – Execute
pnpm run db:migrate:localto apply pending migrations to your local D1 database file (.wrangler/*.sqlite). The local configuration indrizzle.local.config.tsdirects this operation to your development environment. -
Test Functionality – Launch the application with
pnpm devorpnpm dev:cfand verify that the schema changes work correctly through the UI or API endpoints. -
Deploy via CI/CD – Push your changes to trigger the GitHub Actions workflow in
.github/workflows/deploy.yml. The pipeline executespnpm run db:migrate:prod, which runswrangler d1 migrations apply next-cf-app --remoteto apply only pending migrations to the production database. -
Automated Schema Synchronization – The deployment scripts ensure
db:migrate:prodruns 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 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 contains the credentials and connection details for the remote Cloudflare D1 instance. The db:migrate:prod script executes:
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 to include the new column:
// 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
pnpm run db:generate:named "add_priority_level_to_todos"
This creates src/drizzle/0001_add_priority_level_to_todos.sql containing:
ALTER TABLE `todos` ADD `priority_level` integer NOT NULL DEFAULT 0;
Step 3: Test Locally
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
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– Central barrel file that re-exports all table definitions for Drizzle ORM consumption.src/modules/todos/schemas/todo.schema.ts– Example module-specific schema demonstratingsqliteTabledefinitions.src/drizzle/0000_initial_schemas_migration.sql– Baseline migration showing the SQL format expected by the system.drizzle.config.ts– Production configuration connecting to the remote Cloudflare D1 database.drizzle.local.config.ts– Development configuration targeting the local.wrangler/*.sqlitefile.package.json– Defines automation scripts includingdb:generate,db:migrate:local, anddb:migrate:prod..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) and export them viasrc/db/schema.tsfor type safety. - Generate migrations using
pnpm run db:generateto create auditable SQL files insrc/drizzle/. - Test changes locally with
pnpm run db:migrate:localusing the configuration indrizzle.local.config.ts. - Deploy to production via CI/CD executing
wrangler d1 migrations apply next-cf-app --remotethrough thedb:migrate:prodscript. - 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 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →