Skip to content

Running Migrations

Running migrations is the process of applying your schema changes to an actual database. Whether you’re testing locally or deploying to production, understanding how to run migrations confidently is essential.

Local Migrations

Local migrations run against a D1 database stored on your machine. This is your development sandbox—experiment freely without affecting anyone else.

Applying Local Migrations

Use the wrangler d1 migrations apply command with the --local flag:

Terminal
pnpm wrangler d1 migrations apply DB --local

Expected output when migrations are applied:

Migrations to be applied:
┌─────────────────────────────────────┐
│ name │
├─────────────────────────────────────┤
│ 0000_initial.sql │
├─────────────────────────────────────┤
│ 0001_add_user_profile.sql │
└─────────────────────────────────────┘
✔ About to apply 2 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on local database DB from .wrangler/state/v3/d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
Applying migration: 0000_initial.sql
Running query 1/2: CREATE TABLE `users`...
✔ Query 1/2 succeeded
Running query 2/2: CREATE TABLE `posts`...
✔ Query 2/2 succeeded
✔ Applied 0000_initial.sql
Applying migration: 0001_add_user_profile.sql
Running query 1/1: ALTER TABLE `users`...
✔ Query 1/1 succeeded
✔ Applied 0001_add_user_profile.sql
✅ 2 migration(s) applied successfully

Expected output when no migrations are pending:

Migrations to be applied:
┌──────┐
│ name │
├──────┤
│ None │
└──────┘
✔ No migrations to apply

The Local Database Location

When you run local migrations, Wrangler stores the SQLite database in your project:

.wrangler/
└── state/
└── v3/
└── d1/
└── miniflare-D1DatabaseObject/
└── <database-id>.sqlite

This file is automatically created and managed. It’s listed in .gitignore by default—never commit it.

Local Development Workflow

Here’s a typical local development cycle:

flowchart LR
A["Edit schema.ts"] --> B["Generate migration"]
B --> C["Apply locally"]
C --> D["Test your app"]
D --> E{Works?}
E -->|No| A
E -->|Yes| F["Commit changes"]
  1. Edit your schema: Modify src/db/schema.ts
  2. Generate: Run pnpm drizzle-kit generate
  3. Apply: Run pnpm wrangler d1 migrations apply DB --local
  4. Test: Start your dev server and verify the changes
  5. Repeat: If something’s wrong, adjust and regenerate

Remote Migrations

Remote migrations apply changes to your actual Cloudflare D1 databases—staging, production, or any other environment you’ve configured.

Understanding Environments

Your wrangler.toml can define multiple environments:

wrangler.toml
name = "my-app"
main = "src/index.ts"
# Default environment (production)
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Staging environment
[env.staging]
[[env.staging.d1_databases]]
binding = "DB"
database_name = "my-app-db-staging"
database_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

Applying Migrations to Staging

Always test on staging before production:

Terminal
pnpm wrangler d1 migrations apply DB --remote --env staging

Expected output:

Migrations to be applied:
┌─────────────────────────────────────┐
│ name │
├─────────────────────────────────────┤
│ 0002_add_comments.sql │
└─────────────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on remote database DB (yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy):
Applying migration: 0002_add_comments.sql
Running query 1/1: CREATE TABLE `comments`...
✔ Query 1/1 succeeded
✔ Applied 0002_add_comments.sql
✅ 1 migration(s) applied successfully

Applying Migrations to Production

Once verified on staging, apply to production:

Terminal
pnpm wrangler d1 migrations apply DB --remote

Without --env, Wrangler uses the default environment (production).

Expected output:

Migrations to be applied:
┌─────────────────────────────────────┐
│ name │
├─────────────────────────────────────┤
│ 0002_add_comments.sql │
└─────────────────────────────────────┘
✔ About to apply 1 migration(s)
Your database may not be available to serve requests during the migration, continue? … yes
🌀 Executing on remote database DB (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx):
Applying migration: 0002_add_comments.sql
Running query 1/1: CREATE TABLE `comments`...
✔ Query 1/1 succeeded
✔ Applied 0002_add_comments.sql
✅ 1 migration(s) applied successfully

Skipping Confirmation Prompts

For CI/CD pipelines, use the --yes flag to skip confirmation:

Terminal
pnpm wrangler d1 migrations apply DB --remote --yes

This is useful in automated deployments where no one is there to type “yes”.

Deployment Order

flowchart TD
  A["Commit migrations to Git"] --> B["CI/CD triggered"]
  B --> C["Apply to staging"]
  C --> D["Run integration tests"]
  D --> E{Tests pass?}
  E -->|No| F["Fix and recommit"]
  E -->|Yes| G["Apply to production"]
  G --> H["Verify deployment"]

Key principles:

  1. Local first: Always test migrations locally before committing
  2. Staging second: Apply to staging and run tests
  3. Production last: Only after staging is verified
  4. Same migrations everywhere: Never modify migrations between environments

Checking Migration Status

Before running migrations, check what’s pending. This helps you understand the current state of your database.

Listing Local Migrations

Terminal
pnpm wrangler d1 migrations list DB --local

Expected output:

Migrations:
┌──────────────────────────────┬────────────────────┐
│ name │ status │
├──────────────────────────────┼────────────────────┤
│ 0000_initial.sql │ ✅ Applied │
├──────────────────────────────┼────────────────────┤
│ 0001_add_user_profile.sql │ ✅ Applied │
├──────────────────────────────┼────────────────────┤
│ 0002_add_comments.sql │ ⏳ Pending │
└──────────────────────────────┴────────────────────┘

Listing Remote Migrations

Check staging or production status:

Terminal
# Staging
pnpm wrangler d1 migrations list DB --remote --env staging
# Production
pnpm wrangler d1 migrations list DB --remote

Expected output (production with all applied):

Migrations:
┌──────────────────────────────┬────────────────────┐
│ name │ status │
├──────────────────────────────┼────────────────────┤
│ 0000_initial.sql │ ✅ Applied │
├──────────────────────────────┼────────────────────┤
│ 0001_add_user_profile.sql │ ✅ Applied │
├──────────────────────────────┼────────────────────┤
│ 0002_add_comments.sql │ ✅ Applied │
└──────────────────────────────┴────────────────────┘

Comparing Environments

A useful practice is to compare migration status across environments:

Terminal
# Check all environments at once
echo "=== Local ===" && pnpm wrangler d1 migrations list DB --local
echo "=== Staging ===" && pnpm wrangler d1 migrations list DB --remote --env staging
echo "=== Production ===" && pnpm wrangler d1 migrations list DB --remote

This helps identify if migrations are out of sync between environments.

Troubleshooting Common Errors

Even with careful planning, migration issues can occur. Here are the most common problems and their solutions.

1. “No migrations folder found”

Error message:

Error: No migrations folder found at ./migrations

Cause: Wrangler can’t find the migrations directory.

Solutions:

  1. Ensure migrations have been generated:

    Terminal window
    pnpm drizzle-kit generate
  2. Verify the migrations directory exists and contains .sql files:

    Terminal window
    ls -la migrations/
  3. Check your wrangler.toml points to the correct path:

    [[d1_databases]]
    binding = "DB"
    database_name = "my-db"
    database_id = "..."
    migrations_dir = "migrations" # Ensure this matches your folder

2. “Migration already applied”

Error message:

Error: Migration 0000_initial.sql has already been applied

Cause: You’re trying to apply a migration that’s already in the database.

Solutions:

  1. This is usually not an error—check your migration status:

    Terminal window
    pnpm wrangler d1 migrations list DB --local
  2. If you see all migrations as applied, you’re up to date. Nothing more to do.

  3. If you modified a migration file after applying it (don’t do this!), you’ll need to either:

    • Reset your local database (delete .wrangler/state/v3/d1/)
    • Create a new corrective migration

3. SQL Syntax Errors

Error message:

Error: near "ADDING": syntax error
at migrations/0002_add_column.sql:1

Cause: Invalid SQL syntax in the migration file.

Solutions:

  1. Review the SQL file for typos:

    Terminal window
    cat migrations/0002_add_column.sql
  2. Common D1/SQLite syntax issues:

    • Use integer not INT for auto-increment primary keys
    • Use backticks (`) for identifiers, not double quotes
    • No ENUM type—use text with CHECK constraints
    • No BOOLEAN—use integer (0 or 1)
  3. Test your SQL directly:

    Terminal window
    pnpm wrangler d1 execute DB --local --command "SELECT 1"

4. “Database not found”

Error message:

Error: Database DB not found. Please check your wrangler.toml configuration.

Cause: The D1 database binding or ID is misconfigured.

Solutions:

  1. Verify your wrangler.toml has the correct binding name:

    [[d1_databases]]
    binding = "DB" # This must match the command
    database_name = "my-db"
    database_id = "..."
  2. For remote databases, ensure the database exists:

    Terminal window
    pnpm wrangler d1 list
  3. If the database doesn’t exist, create it:

    Terminal window
    pnpm wrangler d1 create my-db

5. “Foreign key constraint failed”

Error message:

Error: FOREIGN KEY constraint failed
at migrations/0003_add_orders.sql:15

Cause: The migration references a table or column that doesn’t exist yet.

Solutions:

  1. Check migration order—foreign key tables must be created after referenced tables

  2. Ensure the referenced table and column exist:

    Terminal window
    pnpm wrangler d1 execute DB --local --command ".schema users"
  3. If migrations are out of order, you may need to:

    • Rename migration files to fix the sequence
    • Regenerate migrations with proper dependencies

6. “Table already exists”

Error message:

Error: table users already exists
at migrations/0000_initial.sql:1

Cause: The migration is trying to create a table that already exists.

Solutions:

  1. For local development, reset the database:

    Terminal window
    rm -rf .wrangler/state/v3/d1
    pnpm wrangler d1 migrations apply DB --local
  2. Use CREATE TABLE IF NOT EXISTS for idempotent migrations (though this can hide issues)

  3. Check if migrations were partially applied—you may need to manually fix the database state

7. “Permission denied” on Remote

Error message:

Error: You do not have permission to access this database

Cause: Authentication or authorization issues with Cloudflare.

Solutions:

  1. Re-authenticate with Cloudflare:

    Terminal window
    pnpm wrangler login
  2. Verify you have access to the account:

    Terminal window
    pnpm wrangler whoami
  3. Check that the database belongs to your account:

    Terminal window
    pnpm wrangler d1 list

8. Migration Timeout

Error message:

Error: Migration timed out after 30 seconds

Cause: A large migration or slow network connection.

Solutions:

  1. Break large migrations into smaller pieces
  2. For data migrations with many rows, process in batches
  3. Check your network connection for remote migrations
  4. Try again—transient network issues are common

Quick Reference

TaskCommand
Apply migrations locallypnpm wrangler d1 migrations apply DB --local
Apply to stagingpnpm wrangler d1 migrations apply DB --remote --env staging
Apply to productionpnpm wrangler d1 migrations apply DB --remote
Apply without confirmationpnpm wrangler d1 migrations apply DB --remote --yes
List local migrationspnpm wrangler d1 migrations list DB --local
List remote migrationspnpm wrangler d1 migrations list DB --remote
Check migration status (staging)pnpm wrangler d1 migrations list DB --remote --env staging

Summary

Running migrations is straightforward once you understand the commands:

  • Local migrations use --local and affect only your development database
  • Remote migrations use --remote and affect actual Cloudflare D1 databases
  • Environment flags (--env staging) target specific environments
  • Status checks with migrations list show what’s applied and pending
  • Troubleshooting usually involves checking syntax, configuration, or database state

With these tools at your disposal, you can confidently evolve your database schema across all environments.