D1 foundations

Create and bind local and remote databases

Keep development state separate from production while using one typed binding name in Worker code.

Every D1 project has two databases from day one, even if you only create one. Wrangler keeps a local copy on your machine for wrangler dev, and Cloudflare keeps the remote one your deployed Worker uses. Same name, same schema, different data. Most D1 mistakes come from forgetting which one a command is talking to.

Local is the default. wrangler dev and any wrangler d1 command without a flag hit the local copy, stored under .wrangler/state. Add --remote and the same command changes production.

Keep the local and remote commands visibly different:

npx wrangler d1 create notes
npx wrangler d1 execute notes --local --command 'select 1'
npx wrangler d1 execute notes --remote --command 'select 1'

Add the returned binding to the Worker configuration. Run migrations locally first. Before every remote command, read the database name and the —remote flag aloud; this small habit prevents destructive experiments against the wrong database.

The first command prints a block of configuration. Paste it into wrangler.jsonc:

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "notes",
      "database_id": "3f1c2a9e-7b4d-4e8a-9c1d-2b5e6f7a8c90"
    }
  ]
}

binding is the name your code uses. Your Worker sees env.DB, and it never has to know whether that points at the local or the remote database. Wrangler decides based on how you run it.

Then regenerate the types, so env.DB shows up as a D1Database in your editor:

npx wrangler types

Do this every time wrangler.jsonc changes. Otherwise TypeScript keeps believing in the old bindings.

Prove the remote database is empty

Run the two select 1 commands from above. Both return a table with a single 1, which proves the binding works on both sides. Now try a query that needs a table:

npx wrangler d1 execute notes --remote --command 'select count(*) from notes'
# ✘ [ERROR] no such table: notes

That error is the answer you want. The remote database is empty, and it stays empty until you apply migrations to it on purpose. That’s the job of the next module.

My habit for scripts: never rely on the default. Write --local even when it’s implied. Then any command without a flag stands out in review, and a --remote in a script is a decision someone typed, not an accident.

Lesson completed