Migrate data and configuration
Evaluate changes to table structures, configuration, resources, and business identifiers, and design a verifiable migration path for an existing project.
Compiling the template source does not prove that the new code can use your existing project's data. This article covers what to check when a future update changes structures or configuration. If there are no such changes, you do not need to create additional migration files.
1. List the actual changes
| Change | What to do |
|---|---|
| Page copy or styles that do not affect interfaces | Usually no database migration is needed |
| New optional configuration options | Check default behavior, schemas, and callers |
| Renamed configuration fields or changed types | Update actual configuration, schemas, code that reads the values, and diagnostic scripts together |
| New columns, constraints, indexes, or states | Design incremental SQL and check whether existing data meets the requirements |
| Renamed keys for products, roles, entitlements, or similar entities | Check configuration, code, and persisted records together |
| Changes to Queue, Event, or Command data formats | Check queued messages and old execution records in the database |
| Changes to APIs or authentication rules | Check callers, existing sessions, tokens, and failure responses |
Even adding a field can break compatibility. For example, a new required field without a default may cause existing configuration or data to fail validation. Judge compatibility by whether existing usage continues to work.
2. Understand database baselines and migrations
The current template has two baselines:
| Binding | Baseline file | Migration naming |
|---|---|---|
DB | schema/db-init.sql | NNNN-db-description.sql |
ANALYTICS_DB | schema/analytics-init.sql | NNNN-analytics-description.sql |
Migration files live in schema/migrations. Four-digit numbers determine execution order, and descriptions use lowercase letters, digits, and hyphens. If your project already has migrations, check their numbers, filenames, and execution history before importing upstream files. Do not overwrite existing files with upstream files of the same name.
The current migration script runs the baseline on an empty database, then applies matching migrations. If an existing database contains the corresponding baseline table, it applies only migrations that have not run. If a nonempty database is missing saas_user or analytics_session, the script stops and requires you to check the database's origin first.
Baseline files contain statements that drop old tables. Do not rerun a baseline to upgrade an existing database. Editing a baseline also does not automatically change table structures already in production.
Validate both existing and empty databases
If you add a column directly to a new baseline while keeping a migration that unconditionally adds the same column, initialization of an empty database may try to add it twice. The current script does not automatically detect which historical changes a baseline already includes.
Whenever you change a baseline, you must therefore validate both paths separately:
- Upgrade from your project's actual existing schema while preserving its data.
- Initialize an empty database and apply migrations to produce a usable schema.
Do not delete migration records or arbitrarily mark migrations as executed to make both paths appear successful. If you need to change the baseline strategy, design it explicitly and retain reproducible test results.
3. Design migrations around business rules
Before writing SQL, establish what the old code reads and writes, what the new code needs, and where values missing from historical data will come from.
For example, if a business table needs an optional note, you can add a nullable column first and then have the new code use it. Before adding a non-null or unique constraint, you must confirm that existing records meet it. Testing the SQL only on an empty database is not enough.
Rebuilding a table must also preserve primary keys, foreign keys, indexes, default values, and state constraints. Do not copy only column names. Missing indexes or constraints may surface as slow queries or duplicate data only after deployment.
Preserve the history of migrations already run in production or another shared environment. Add a subsequent migration when a correction is needed. The target database records which migrations have run. A local file's existence does not prove that its migration has completed in production.
For specific queries and troubleshooting steps, see Database and migration troubleshooting. For D1's migration mechanism, see Cloudflare D1 migrations.
4. Validate migrations without affecting live operations
Prepare isolated test data with a schema that represents the version before the upgrade. Include null values, historical states, related records, and business data that has already been used. Newly created empty tables alone are not sufficient.
After confirming that the command targets local test state, run:
npm run db:migrate:localCheck migration records, the final schema, counts of key records, and business relationships. Then use the application to read and write this data. For permission and payment changes, also check whether repeated execution grants anything twice or incorrectly reverts a state.
This command processes both local databases. If the business database succeeds and the analytics database fails, do not assume the earlier changes were undone. Production migrations are not a single transaction across both databases either.
To test an empty database, create a separate isolated local environment. Do not clear the development database you are using just to complete the validation steps.
5. Maintain compatibility throughout deployment
The current deploy:update runs remote migrations before deploying the new Worker. This means the old code may continue handling requests after the SQL has run.
Adding new structures is usually easier to plan than immediately removing old ones. For example, if you need to replace an existing field, consider a phased rollout: prepare the new structure and adapt the code, convert the data, then remove the old structure once you have confirmed that no old references remain.
This approach is for releases that actually need migrations. It does not require a compatibility layer for every update. Each phase should have completion criteria, with explicit plans to remove temporary read logic and old fields.
If the old Worker cannot read the new schema, you cannot claim that a failed deployment can be recovered by simply switching back to the old code. Redesign the migration order or prepare a separate, controlled switchover plan for that change.
6. Update configuration while preserving project identity
Check these locations when handling configuration changes:
- Project values in
config/and validation insrc/libs/config/schemas/. - Server and client configuration exports, along with callers in pages, APIs, services, and scripts.
example.varsand the actual.envand.env.productionfiles.- Bindings in
wrangler.jsoncand check rules inscripts/doctor/.
Do not overwrite your project's values with the new template's branding, administrator email addresses, products, or resource names. When adding a configuration field, understand its default behavior before deciding whether to enable it for your project.
Configure new secrets using your existing secure process. You must preserve your project's existing SAAS_SECRET. It is not a version identifier to regenerate during an upgrade.
7. Environment variables and Cloudflare resources
example.vars is only a variable template. It does not automatically merge new fields into existing environment files. When renaming a variable, update the code that reads it, check scripts, environment files, and deployment behavior together.
Variables starting with VITE_ may be included in browser build output and must contain only public values. Removing an entry from .env.production or leaving it empty does not automatically delete an existing remote Worker Secret. Check how it is actually used in production and handle its removal separately.
After changing bindings, run:
npm run cf-typegen
npm run doctorRun npm run doctor:remote when preparing for production deployment. These checks do not automatically create every missing resource and cannot replace verification of the actual resources.
| Resource change | Specific checks |
|---|---|
| D1, KV | Preserve the original project's resource IDs and determine whether data needs to move |
| R2 | Ensure MAIN_R2.bucket_name matches R2_BUCKET_NAME and existing files remain accessible |
| Queue | Ensure producers, consumers, and *_QUEUE_NAME match, and old message formats can still be processed |
| Durable Object | Check exported class names, bindings, and migration declarations. Do not overwrite existing migration history |
| Cron | Ensure Wrangler expressions match the string-based dispatch conditions in src/entry/index.ts |
Creating remote resources is a separate implementation step. Do not assume cf-typegen creates resources or rerun initial deployment for an existing project as a substitute for designing a migration.
8. Treat business keys as data identifiers
Product IDs, plan IDs, roles, entitlements, capabilities, and scopes may all be stored in databases or external systems. Changing a key is more than changing display text.
Before renaming one, search definitions, references, stored database values, and event payloads. For example, removing a plan mapping for an old Price may affect callbacks for old orders and processing of existing subscriptions. Changing STRIPE_CONNECTION_ID also changes which payment records are identified by that connection.
When Queue formats change, check old versions and payloads in event_execution and command_execution as well. Updating only the code that creates new requests does not guarantee that tasks queued before the upgrade can still run.
Keep stable identifiers unless this update specifically requires a change. If a migration is necessary, document its scope, how old records will continue to be processed, and what happens on repeated execution in the change plan.
9. Prepare recovery materials in advance
Data protection should cover what this update will actually change. D1 backups do not include R2 files, KV content, Queue messages, Secrets, or transactions that have already occurred in Stripe.
Prepare database exports or usable recovery points for future production migrations, and verify the recovery procedure. D1 provides Time Travel, but you should confirm the window available to your account and the target recovery point at the time of the operation. Do not assume you can restore to any point in history. See Time Travel and backups.
Restoring a database may also lose legitimate writes made after the recovery point. Reconcile new users, payments, and background operations from that period. Database restoration is not an automatic part of a normal code rollback.
After completing these preparations, continue to Validation, deployment, and maintenance records.