Database: D1
Declare a database in your config, then query it directly from request handlers.
Set up D1
D1 is Cloudflare's managed SQLite database. Add a declaration to your existing config, keeping your app's name and compatibility date:
import { defineConfig } from "goribu/server";
export default defineConfig({
name: "my-app",
compatibilityDate: "2026-09-01",
database: { type: "d1", name: "my-app-db" },
});Restart development after changing bindings. Goribu provides a local database and generates the DB binding. Deployment creates the named production database if it is missing, or reuses it. Local data is separate and is not uploaded.
Create a migration before querying a table. The tutorial walks through a complete example.
Read rows
Use the req.d1 tagged template in a handler or middleware. Interpolated values become SQL parameters:
const post = await req.d1`
SELECT id, title FROM posts WHERE id = ${req.params.id}
`.get();
const posts = await req.d1`
SELECT id, title FROM posts ORDER BY id DESC
`.all();| Terminal method | Result |
|---|---|
.get() | First row or null. |
.all() | Array of rows, possibly empty. |
.value() | First column of the first row or null. |
.run() | Write result with changes and lastRowId. |
Queries are lazy. Choose a terminal method; awaiting the tagged statement by itself is an error.
Strings, numbers, booleans, null, and binary values are supported. Booleans bind as 1 or 0. Convert dates to strings and structured data with JSON.stringify() first. A non-empty array expands into placeholders, which is useful for IN (${ids}); empty and nested arrays are rejected.
Write rows
Use .run() for inserts, updates, and deletes:
const title = "Hello Goribu";
const { lastRowId } = await req.d1`
INSERT INTO posts (title) VALUES (${title})
`.run();
await req.d1`
UPDATE posts SET title = ${"Updated title"} WHERE id = ${lastRowId}
`.run();Validate submitted values before writing. Return a response only after the write succeeds. Recognized SQLite constraint failures include useful fields such as constraint, table, columns, and constraintName; inspect the error rather than treating every database failure as validation.
Atomic batches
Use req.d1.atomic() when several statements must succeed together. Pass unexecuted statements, without .run() or another terminal:
await req.d1.atomic([
req.d1`INSERT INTO posts (title) VALUES (${"First post"})`,
req.d1`INSERT INTO posts (title) VALUES (${"Second post"})`,
]);The result has one entry per statement, each containing rows, changes, and lastRowId. This is a batch, not an interactive transaction: you cannot read a result, branch in JavaScript, and continue that same atomic operation.
Replication and freshness
Goribu enables replication for a newly created database unless replication: false is configured. Omit the option to leave an existing database's policy unchanged, or set true/false to reconcile it on deployment.
With replication enabled, default queries share a request-scoped session. Goribu carries its position in a cookie so a visitor's following requests can see their own writes, including after a redirect.
| Mode | Use |
|---|---|
req.d1 | Normal application data with the visitor's session bookmark. |
req.d1.fresh | Read from the primary when the latest committed state matters. |
req.d1.public | Shared data where briefly stale replica reads are acceptable. |
const account = await req.d1.fresh`
SELECT * FROM accounts WHERE id = ${accountId}
`.get();Use fresh reads for decisions such as authorization, balances, or quotas. public does not make the HTTP response cacheable. Freshness modes cannot be chained. With replication disabled, all modes use the raw database binding and no bookmark cookie is set.
Migrations
Create the next numbered migration:
pnpm exec goribu migrate:new create postsFill in its SQL. Keep both markers; the down section may be empty when the change cannot safely be reversed.
-- migrate:up
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
);
-- migrate:down
DROP TABLE posts;pnpm exec goribu migrate
pnpm exec goribu migrate:statusThese commands target local D1. Add --remote to deliberately inspect or modify production. Deployment applies all pending production migrations before uploading the Worker.
Applied migration files are history: do not edit, remove, or insert earlier files. Checksums detect modified, missing, and out-of-order migrations. Restore the original history and make corrections in a new migration.
pnpm exec goribu migrate:down rolls back one migration if it has down SQL. Read that SQL first; a rollback may discard data. Put repeatable local test data in migrations/seed.sql and run pnpm exec goribu migrate:seed. Seeding cannot target production.
Each migration and its tracking record are atomic together. Separate files are separate batches, and a Worker deployment is not part of that transaction. Use additive changes first, deploy code that adopts them, and remove old columns only in a later compatible release. See Deploys.
Dynamic SQL
Use parameters for values. If SQL structure must vary, choose fragments from your own allowlist and use unsafe(text, params):
const column = req.query.get("sort") === "title" ? "title" : "id";
const posts = await req.d1
.unsafe(`SELECT id, title FROM posts ORDER BY ${column} LIMIT ?`, [20])
.all();Never interpolate request input directly into the SQL text. unsafe() does not make arbitrary SQL safe.
Raw access
req.d1.session exposes the default session for a query builder. fresh.session starts a session at the primary; subsequent operations may use replicas while remaining sequentially consistent. Each terminal on a tagged req.d1.fresh query starts a new primary session.
req.d1.binding exposes the plain Cloudflare binding. Prefer the session when the integration supports it so you keep the intended consistency behavior.