Skip to content
Outer
Esc
↑↓navigate↵open⌘Jpreview
On this page

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

Was this page helpful?