Skip to content

Connections ​

A query finds its D1 database in one of three ways. Most apps only use the first.

HowUse it for
The bindingUser.find(id), with nothing to set upEveryday queries
The request’s databasewithDatabase(connect(env.DB, …), fn), once per requestRead replicas with a Session, per-request onQuery logging, tests, and scripts
An explicit databaseconnect(env.DB).model(User).find(id)Code that passes its database around, or uses two at once

Database resolution explains why there are three, and which one you need.

Using the binding ​

Each Model names the D1 binding it queries, so a Model queries by itself during any request:

ts
import { Post, User } from "./models";

// No setup: each Model queries its binding (static binding = "DB") during the request.
const user = await User.findByOrThrow({ email: "ada@example.com" });
const posts = await Post.where({ userId: user.id }).order({ id: "desc" }).toArray();

d1-record schema sets the binding on your app’s base class from your Wrangler config’s d1_databases, as static override binding = "DB".

Records remember their database, so user.save() and user.posts() use the same one later. Build records with User.build(…): a record made with new User() has no database, and throws NoDatabase when it needs one.

TIP

The binding is only there during a request, inside a Worker. Tests and scripts that run in Node get their database from withDatabase. See Testing.

Using the request’s database ​

Some things belong to one request, such as a D1 Session that reads its own writes, or logging tagged with the request’s ID. Open a database for the request, and run its code inside withDatabase:

ts
import { connect, withDatabase } from "@forestfuture/d1-record";

export default {
  fetch(request, env) {
    const db = connect(env.DB, { onQuery: log });
    return withDatabase(db, () => app.fetch(request, env));
  },
};

Every query started from a Model inside it, and everything it awaits, uses that database instead of the binding.

Node.js compatibility

withDatabase uses Workers’ AsyncLocalStorage. It’s on by default for compatibility dates from 2026-08-04. For an earlier date, add "compatibility_flags": ["nodejs_compat"] to your Wrangler config, or withDatabase throws NoDatabase. The rest of d1-record doesn’t need it.

These pages use a request’s database:

Using an explicit database ​

Start a query from a database you hold with db.model. It ignores withDatabase:

ts
import { connect } from "@forestfuture/d1-record";
import { User } from "./models";

const db = connect(env.DB);
const user = await db.model(User).findByOrThrow({ email: "ada@example.com" });

To name your Models once, use db.models:

ts
import { Post, User } from "./models";

const models = db.models({ User, Post });
const posts = await models.Post.where({ userId: user.id }).toArray();

connect(env.DB, { session, onQuery }) takes the same options as a request’s database.

Batching writes ​

batch writes statements and records atomically, on the database they were built on:

ts
import { batch } from "@forestfuture/d1-record";
import { Post, User } from "./models";

const user = User.build({ email: "grace@example.com" });
const post = Post.build({ userId: user.id, title: "Hello" });

// Atomic: all written, or none.
await batch([user.toSave(), post.toSave(), Post.where({ userId: "old" }).toDeleteAll()]);

Entries from a Model’s statics use the current database. db.batch([...]) uses an explicit one. See Bulk writes and batches.