Moving a Cloudflare D1 database to another Cloudflare account is two wrangler commands — but the second one almost always fails on the first try, for a reason that isn’t obvious.
When you’d do this#
Three situations come up:
- Account split — splitting a shared Cloudflare account into per-project ones (often when a side project graduates to a company account, or vice versa).
- Staging clone — copying production’s shape and a snapshot of data into a staging database on the same or a different account.
- Organisation change — moving a project’s infrastructure under a different billing entity without recreating the schema by hand.
Cloudflare has no built-in “transfer this D1 database” button. The path is: export to SQL, create the empty database on the destination, replay the SQL.
The two commands#
From the account that owns the source database:
npx wrangler d1 export <database_name> --remote --output=./database.sqlThen authenticate as the destination account (log out and log in again with wrangler logout / wrangler login, or use a different CLOUDFLARE_API_TOKEN), create the new D1 database (npx wrangler d1 create <new_database_name>), and replay:
npx wrangler d1 execute <new_database_name> --remote --file=./database.sqlThat’s the happy path. It usually isn’t.
The foreign-key ordering error#
On the second command you’ll often see something like:
no such table: main.user: SQLITE_ERROR…even though the export ran cleanly and user is clearly in the file.
Why it happens. wrangler d1 export writes tables in the order SQLite returns them from sqlite_schema, which is creation order — not dependency order. If you ever ran a migration that created user_role (with a foreign key to user) before creating user itself, the export preserves that ordering. On replay, the CREATE TABLE user_role statement runs first, references a user table that doesn’t exist yet, and aborts the whole batch.
How to fix. Open database.sql in an editor and move the CREATE TABLE user statement above the CREATE TABLE user_role statement. More generally: parent tables before child tables, for every FK relationship in the file. Once the schema statements are correctly ordered, the INSERT statements below them are safe — row inserts don’t need the same reshuffling because FK checks usually permit referenced rows to appear in the same transaction.
If you have more than two or three FK chains, it’s faster to dump schema and data separately, hand-order the schema file, and replay in two passes:
npx wrangler d1 export <database_name> --remote --no-data --output=./schema.sql
npx wrangler d1 export <database_name> --remote --no-schema --output=./data.sql
# hand-edit schema.sql, then:
npx wrangler d1 execute <new_database_name> --remote --file=./schema.sql
npx wrangler d1 execute <new_database_name> --remote --file=./data.sqlThings that don’t make the trip#
- Database ID changes. The new D1 has a new
database_id. Update yourwrangler.jsonc(orwrangler.toml) on the destination account to match — the old ID only exists on the old account. - Migration history. D1 tracks applied migrations in a
d1_migrationstable. After replay the new database “knows” the same migrations were applied (because that table is in the dump), but if you ran any migrations on the source after the export, you’ll need to run them against the new database separately. - Insert performance.
wrangler d1 execute --file=replays statements one at a time over HTTP. For a 50k-row database expect several minutes; for a 500k-row database expect closer to an hour. The command is resumable if you split the file — use the--no-data/--no-schemasplit above and then chunkdata.sqlif needed.
Verify before you delete#
Before deleting the source database, run a few sanity queries against the new one:
npx wrangler d1 execute <new_database_name> --remote --command "SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name"
npx wrangler d1 execute <new_database_name> --remote --command "SELECT COUNT(*) FROM user"Table list matches, row counts match — then you’re done.
References#
Related posts
- Setting up D1 Database with Drizzle in a Hono Cloudflare Worker AppThis guide covers configuring a D1 database using Drizzle in a Hono app deployed using Cloudflare Workers.cloudflare workerscloudflare D1honodrizzle
- Integrate Better Auth and Google One Tap with Hono and SvelteKitA walkthrough of wiring Better Auth with Google One Tap into a SvelteKit app, using Hono for the API layer and Cloudflare D1 + Workers for the backend.better authgoogle one taphonosveltekitdrizzlecloudflare workerscloudflare D1
- Cloudflare Cron Jobs in SvelteKitHow to add cron job support to a SvelteKit app deployed on Cloudflare Workers, since the adapter doesn't natively support the scheduled event handler.sveltekitcloudflare workerscron