Cloudflare / D1 recipe
D1 recipe
The pokedex example’s team roster: native uses bun:sqlite, Workers use D1. Both sit behind one async TeamDb interface.
1Migration
migrations/0001_team.sql
CREATE TABLE IF NOT EXISTS team (id INTEGER PRIMARY KEY, name TEXT NOT NULL, display_name TEXT NOT NULL,
num TEXT NOT NULL, types TEXT NOT NULL, artwork TEXT NOT NULL, added_at INTEGER NOT NULL);2Binding
In your wrangler.jsonc (wrangler d1 create <name> prints the database_id):
wrangler.jsonc
{ "d1_databases": [{ "binding": "TEAM_DB", "database_name": "pokedex-team", "database_id": "<paste the id>", "migrations_dir": "migrations" }] }3The D1 module
The binding is read per call. The cap of six members is one conditional statement, because D1 has no transaction handle across awaits:
lib/team-db.d1.ts
import { env } from 'cloudflare:workers'
const INSERT =
'INSERT INTO team (id, name, display_name, num, types, artwork, added_at) ' +
'SELECT ?1, ?2, ?3, ?4, ?5, ?6, MAX(?7, (SELECT COALESCE(MAX(added_at), 0) + 1 FROM team)) ' +
'WHERE (SELECT COUNT(*) FROM team) < ?8 ON CONFLICT(id) DO NOTHING'
// inside add(): one batch is one transaction. 1 row changed = added; else look the id up to tell present from full.
const [ins, has] = await env.TEAM_DB.batch([
env.TEAM_DB.prepare(INSERT).bind(m.id, m.name, m.displayName, m.num, JSON.stringify(m.types), m.artwork, Date.now(), 6),
env.TEAM_DB.prepare('SELECT 1 AS one FROM team WHERE id = ?1').bind(m.id),
])4Alias
brust.toml
[cloudflare.alias]
"lib/team-db.ts" = "lib/team-db.d1.ts"Mark the sqlite module server_only so it never reaches a browser bundle.
5Apply and run
~/pokedex
$ wrangler d1 migrations apply pokedex-team --local -c dist/cf/wrangler.jsonc # local workerd
$ bun run dev:cf
$ wrangler d1 migrations apply pokedex-team --remote -c dist/cf/wrangler.jsonc # before the first deployBuild once first so dist/cf/wrangler.jsonc exists.
Note
A D1 query counts as a subrequest, and the Free plan allows 50 queries per invocation (source C6 on Workers target, read 2026-10-12).