Connections
A query finds its D1 database in one of three ways. Most apps only use the first.
| How | Use it for | |
|---|---|---|
| The binding | User.find(id), with nothing to set up | Everyday queries |
| The request’s database | withDatabase(connect(env.DB, …), fn), once per request | Read replicas with a Session, per-request onQuery logging, tests, and scripts |
| An explicit database | connect(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:
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:
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:
- Read replicas: a D1 Session per request.
- Logging: one request’s queries, tagged with its ID.
- Testing: a database for Models outside a Worker.
- Multiple databases:
withDatabase([...]), matched to each Model’s binding.
Using an explicit database
Start a query from a database you hold with db.model. It ignores withDatabase:
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:
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:
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.