Skip to content

Workers D1 -- Schema Migrations and Backups

DodaTech 6 min read

In this tutorial, you will learn how to evolve your Cloudflare Workers D1 database schema over time using Wrangler Migration files and how to back up and restore your data safely. Schema migrations are important because they let you alter table structures without losing existing data or breaking running Workers. A real-world example is a blog platform adding a tags column to its posts table while thousands of concurrent readers continue to query the database.

Why Migrations Matter

In production, you cannot simply drop and recreate tables every time your data model changes. D1 migrations allow you to apply incremental schema changes -- adding columns, creating indexes, or seeding data -- in a controlled, repeatable way. Each Migration is a numbered SQL file that Wrangler applies in order. If multiple developers work on the same project, migrations prevent schema drift and ensure every environment is identical. D1 also supports backup and restore via the Wrangler CLI, letting you export the full database to a SQL file or import one.

Migration Workflow

flowchart LR
    A[Start] --> B[Create Migration file]
    B --> C[npx wrangler d1 migrations create]
    C --> D[Write SQL in Migration file]
    D --> E[Apply Migration]
    E --> F[npx wrangler d1 migrations apply]
    F --> G{Check result}
    G -->|Success| H[Database updated]
    G -->|Failure| I[Rollback manually]
    H --> J[Next Migration]

    style A fill:#3498db,color:#fff
    style H fill:#2ecc71,color:#fff
    style I fill:#e74c3c,color:#fff

Wrangler tracks which migrations have been applied in a _d1_migrations table. Each Migration runs exactly once, in sequence, ensuring deterministic schema evolution.

Creating Your First Migration

# Generate a new migration file
npx wrangler d1 migrations create my-database add-tags-to-posts

# Expected output:
# Creating migration: migrations/0001_add-tags-to-posts.sql
# Migration file created

Wrangler creates a timestamp-prefixed SQL file in a migrations/ directory. You then edit the file to contain your schema changes.

-- migrations/0001_add-tags-to-posts.sql
ALTER TABLE posts ADD COLUMN tags TEXT NOT NULL DEFAULT '';
CREATE INDEX idx_posts_tags ON posts(tags);

The ALTER TABLE statement adds a tags column to the posts table. The DEFAULT clause ensures existing rows receive an empty string. A covering index on tags speeds up tag-based queries.

Applying Migrations

// Bind D1 to Worker and query after migration
export default {
  async fetch(request, env) {
    // After migration 0001, the tags column is available
    const { results } = await env.DB.prepare(
      'SELECT id, title, tags FROM posts WHERE tags LIKE ?1'
    ).bind('%cloudflare%').all();

    return new Response(JSON.stringify(results), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

// Request: GET /posts?tag=cloudflare
// Expected response:
// [{"id": 1, "title": "Getting Started", "tags": "cloudflare,workers"}, {"id": 3, "title": "D1 Deep Dive", "tags": "cloudflare,d1"}]

Apply the Migration with Wrangler:

npx wrangler d1 migrations apply my-database --remote

# Expected output:
# Applying migration 0001_add-tags-to-posts.sql...
# Migration applied successfully

The --remote flag applies to the production D1 instance. Omit it to apply to the local development database.

Backing Up and Restoring D1

# Export the full database to a SQL file
npx wrangler d1 export my-database --remote --output ./backup-2026-06-23.sql

# Expected output:
# Exporting database my-database (remote)...
# Exported 15 tables, 1024 rows to backup-2026-06-23.sql

The exported file contains CREATE TABLE and INSERT statements that recreate the full database. Use this for disaster recovery or cloning environments.

# Restore from backup
npx wrangler d1 import my-database --remote --file ./backup-2026-06-23.sql

# Expected output:
# Importing backup-2026-06-23.sql into my-database (remote)...
# Import completed: 15 tables, 1024 rows

Import drops all existing data and recreates the database from the SQL file. Use with caution in production -- always take a fresh backup before restoring.

Seeding Data via Migration

// migrations/0002_seed_categories.sql
// -- Seed initial categories for the blog
// INSERT OR IGNORE INTO categories (id, name, slug) VALUES
//   (1, 'Technology', 'technology'),
//   (2, 'Security', 'security'),
//   (3, 'DevOps', 'devops');

export default {
  async fetch(request, env) {
    const { results } = await env.DB.prepare(
      'SELECT name, slug FROM categories ORDER BY id'
    ).all();

    return new Response(JSON.stringify(results), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

// Expected response:
// [{"name": "Technology", "slug": "technology"}, {"name": "Security", "slug": "security"}, {"name": "DevOps", "slug": "devops"}]

Seed data migrations are idempotent when using INSERT OR IGNORE. The same Migration can be applied multiple times without creating duplicate rows.

Common Errors and Troubleshooting

Migration Already Applied

If you try to apply a Migration that has already run, Wrangler skips it with a message like Migration 0001 already applied, skipping. This prevents duplicate schema changes.

ALTER TABLE Limitations

D1 (backed by SQLite) supports adding columns with ALTER TABLE ... ADD COLUMN but does not support dropping or renaming columns. To remove a column, create a new table, copy data, drop the old table, and rename the new one as part of a Migration.

Failed Migration Recovery

If a Migration fails mid-way (e.g., a syntax error), Wrangler marks it as not applied. Fix the SQL file and re-run. D1 does not support automatic rollback -- manual correction is required.

Export Timeouts

Large D1 databases may time out during export. Use the --no-data flag to export only the schema if data is not needed. For full backups, schedule exports during low-traffic periods.

Backup File Size

Exported SQL files can be large. Compress backups with gzip before archiving. D1 databases up to 1 GB export to SQL files of similar size.

Practice Questions

  1. What Wrangler command creates a new D1 Migration file?
  2. How does D1 track which migrations have already been applied?
  3. What SQLite limitation affects schema changes in D1 migrations?

FAQ

Can I roll back a D1 Migration?

D1 does not support automatic rollback. To undo a Migration, create a new Migration that reverses the schema change (e.g., adding a column requires a table rebuild with a copy-and-rename pattern). Always test migrations on a development database first.

Are D1 backups encrypted?

D1 backup files are plain SQL text. If you store backups outside Cloudflare, encrypt them with tools like GPG or store them in R2 with server-side encryption. For production workloads, automate encrypted backups to an R2 bucket using a Cron Trigger.

What happens to running Workers during a Migration?

D1 migrations are applied independently of Worker deployments. If you add a column and deploy a Worker that queries it simultaneously, the Worker may encounter a schema mismatch for a brief window. Coordinate schema changes with Worker deployments using a versioned release Process.

Summary

D1 migrations provide a structured way to evolve your database schema using Wrangler-generated SQL files applied in sequence. Backups export the full database as SQL for disaster recovery. While D1 supports adding columns and creating indexes, column removal requires a manual table rebuild pattern. Combine migrations with automated backup workflows to maintain production reliability.

This guide is brought to you by the developers of Cloudflare, SQLite Database, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro