Skip to content

Search docs

Jump between documentation pages.

Browse docs

Use Cloudflare D1 with DaloyJS

Cloudflare D1 is a serverless SQLite-compatible database built into Cloudflare Workers. A Worker reaches it through a binding (no network driver, no auth token in your code, no connection to manage), making it the lowest-friction database for the Cloudflare Workers adapter. If you already have Postgres or MySQL, use Hyperdrive instead.

1. Provision via Wrangler

bash
pnpm add -D wrangler
pnpm wrangler d1 create my-app-db

Add the returned binding to your wrangler.jsonc:

wrangler.jsoncjsonc
{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-22",
  "compatibility_flags": ["nodejs_compat"],
  "d1_databases": [
    { "binding": "DB", "database_name": "my-app-db", "database_id": "<id-from-create>" }
  ]
}

2. Type the binding

Generate types from your Wrangler config instead of writing an Env interface by hand:

bash
pnpm wrangler types

This writes worker-configuration.d.ts with your bindings, so env.DB from cloudflare:workers is typed as D1Database without a cast. Rerun it whenever you change wrangler.jsonc, and do not edit the generated file.

3. Decorate the app with the binding

D1 bindings live on env, not process.env. Import env from cloudflare:workers and decorate the app with the binding once, before you register routes. Do not decorate inside the Worker's fetch handler: decorate() throws on the second request, and a decoration added after the routes were registered never reaches them.

Binding flow
  1. 01import { env } from "cloudflare:workers"env.DB is the D1 binding
  2. 02app.decorate('db', env.DB)once, before the routes
  3. 03toFetchHandler(app)route handler reads state.db
  4. 04state.db.prepare(...).all()query D1, no network driver
D1 bindings live on env, not process.env, so import env from cloudflare:workers and decorate the app with env.DB before registering routes.
src/index.tsts
import { z } from "zod";
import { env } from "cloudflare:workers";
import { App, secureHeaders } from "@daloyjs/core";
import { toFetchHandler } from "@daloyjs/core/cloudflare";

declare module "@daloyjs/core" {
  interface AppState {
    db: D1Database;
  }
}

const app = new App({ production: true, behindProxy: { hops: 1 } });
app.use(secureHeaders());
// Decorate before the routes: a route captures its decorations when it is
// registered, and decorate() throws if called again on a later request.
app.decorate("db", env.DB);

const TodoSchema = z.object({ id: z.number(), title: z.string(), done: z.boolean() });

app.get(
  "/todos",
  {
    operationId: "listTodos",
    responses: { 200: { description: "ok", body: z.array(TodoSchema) } },
  },
  async ({ state }) => {
    const { results } = await state.db
      .prepare("select id, title, done from todos")
      .all<{ id: number; title: string; done: number }>();
    return {
      status: 200,
      body: results.map((r) => ({ ...r, done: Boolean(r.done) })),
    };
  },
);

export default toFetchHandler(app);

Keep the declare module block in the Worker module itself, not in a separate .d.ts file. Declaration files are exempt from type-checking when skipLibCheck is on (the scaffolded default), so a mistake inside a .d.ts fails silently and state.db degrades to any. Bind request values with .bind() (prepare("select * from todos where id = ?").bind(id)), never by string concatenation.

Read replication and the Sessions API

D1 can serve reads from replicas in other regions. Turn on read replication in the dashboard (database Settings) or the REST API, then run queries through a session so a client always reads data at least as new as its last write. A session starts from a constraint ("first-unconstrained", the default, or "first-primary") or from a bookmark returned by an earlier session. Cloudflare's example passes the bookmark back and forth in an x-d1-bookmark header. Check Cloudflare's read replication docs for the current release status.

A session is a per-request value, so set it in a hook instead of decorate():

src/index.tsts
declare module "@daloyjs/core" {
  interface AppState {
    d1: D1DatabaseSession;
  }
}

app.use({
  preBody: (ctx) => {
    // The bookmark comes from the client: treat it as untrusted input.
    const bookmark = ctx.request.headers.get("x-d1-bookmark");
    const start = bookmark && bookmark.length <= 256 ? bookmark : "first-unconstrained";
    ctx.state.d1 = env.DB.withSession(start);
  },
  onSend: (res, ctx) => {
    const next = ctx?.state.d1?.getBookmark();
    if (next) res.headers.set("x-d1-bookmark", next);
  },
});

// ...register routes that query state.d1.prepare(...) instead of state.db

Register the hook before the routes. Use "first-primary" for routes that must read the latest committed data. Browser clients on another origin need x-d1-bookmark in the cors() exposedHeaders and allowedHeaders options.

Migrations

bash
pnpm wrangler d1 migrations create my-app-db init
pnpm wrangler d1 migrations apply my-app-db --local
pnpm wrangler d1 migrations apply my-app-db --remote

Run one-off SQL with wrangler d1 execute, against the local copy used by wrangler dev or the real database:

bash
pnpm wrangler d1 execute my-app-db --local --command "select count(*) from todos"
pnpm wrangler d1 execute my-app-db --remote --file ./seed.sql

Backups: Time Travel and export

  • Time Travel restores a database to any minute in the retention window (30 days on Workers Paid, 7 days on Free): pnpm wrangler d1 time-travel info my-app-db and pnpm wrangler d1 time-travel restore my-app-db --timestamp=<unix-or-rfc3339>. A restore overwrites the database in place and cancels in-flight queries.
  • Export a SQL dump for longer retention or local copies: pnpm wrangler d1 export my-app-db --remote --output=./backup.sql. See import and export.

With Drizzle ORM

bash
pnpm add drizzle-orm
src/db/drizzle.tsts
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";

export const db = drizzle(env.DB);

Decorate db before the routes, as above.

With Prisma

Prisma supports D1 through the D1 driver adapter (@prisma/adapter-d1). With Prisma 7, generate the client with the prisma-client generator and an output path, construct new PrismaD1(env.DB) from the imported env, pass it as adapter, and decorate the client before the routes, like the binding above. Prisma Migrate does not apply migrations to D1 directly; generate SQL with prisma migrate diff and apply it with wrangler d1 migrations. See Prisma.

Limitations to know

  • The binding exists only inside Workers. Outside Workers (Node, Lambda, CI scripts) D1 is reachable only through Cloudflare's REST API, which is meant for administration and tooling rather than serving application traffic. Node can also run your Worker locally through wrangler dev.
  • Local development uses wrangler dev with a local SQLite file. Behavior is close but not identical to production.
  • For multi-runtime portability, prefer Turso (libSQL), or Postgres through Hyperdrive on Workers and directly elsewhere.

See also Turso, Cloudflare Hyperdrive, Neon, and the database hosting overview.

Edit this page on GitHub

Last updated