D1 guide
App data in D1 — add the binding, create tables, and query them.
The template ships with no database: pages/index.server.ts keeps
todos in a module-level array. Add D1 like this.
1. Declare the binding
In void.config.ts, uncomment the D1 example and rebuild — bindings are
read from the built config:
import { defineConfig } from "void/config";
export default defineConfig({
cloudflare: {
name: "my-app",
d1_databases: [{ binding: "DB", database_name: "app-db" }],
},
worker: {
compatibility_date: "2026-09-01",
},
});
The built dist/wrangler.json then carries the binding through Outer’s
deploy validation to celld, which creates the database on first use.
celld keys the database by name, so a typo opens a silent empty database
— double-check the name.
2. Create tables in code
Outer’s deploy does not run migration files: its steps are checkout,
bun install, bun run build, the gate, wrangler validation, celld
deploy and a health check. Create tables with exec, the celld pattern:
import { env } from "cloudflare:workers";
const SCHEMA = `
CREATE TABLE IF NOT EXISTS item (
id INTEGER PRIMARY KEY AUTOINCREMENT,
label TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS item_id ON item (id);
`;
export const loader = defineHandler(async () => {
await env.DB.exec(SCHEMA);
const { results } = await env.DB.prepare(
"SELECT id, label FROM item ORDER BY id",
).all();
return { items: results };
});
prepare() takes exactly one statement; bind(...) fills ? placeholders;
all() returns { results }, first() one row, run() writes. exec()
runs parameter-less statements such as schema. writes:
await env.DB.prepare("INSERT INTO item (label) VALUES (?)")
.bind(label.trim())
.run();
Seed data counts as page text: no strong profanity in INSERTs — the
pre-live gate reads SQL string literals and a profane seed blocks the
deploy.
3. Drizzle, locally
Void’s documented query layer is Drizzle: tables in db/schema.ts with
helpers from void/schema-d1, imported through the @schema alias, and
queries through db from void/db:
// db/schema.ts
import { integer, sqliteTable, text } from "void/schema-d1";
export const items = sqliteTable("items", {
id: integer("id").primaryKey({ autoIncrement: true }),
label: text("label").notNull(),
});
import { items } from "@schema";
import { db, eq } from "void/db";
const all = await db.select().from(items);
await db.insert(items).values({ label: "Milk" });
await db.delete(items).where(eq(items.id, 1));
Iterate locally with void db push (applies the schema without migration
files), then void db generate to write db/migrations/*.sql and commit
them. void db migrate applies pending migrations to the local
database only. On Outer, tables still come from step 2’s exec — the
deploy never runs migration files.
Sources
- The template without a database: https://github.com/ryuzcorp/noite-template-ilha/blob/636715ea44d8944701652abda734ab610e75c69d/pages/index.server.ts
- Void’s database guide and CLI reference: https://void.cloud/guide/database, https://void.cloud/reference/cli/database
- celld’s D1 API, limits and first-use creation: https://celld.dev/docs/services/d1/