---
title: D1 guide
description: App data in D1 — the binding is on, tables live in code.
---

The template ships with D1 **on by default**: `void.config.ts` declares
it, and `pages/index.server.ts` is the pattern to copy — a loader that
reads D1 and one action (an `intent` field tells the forms apart) that
writes it. Tables are created in code; nothing is kept in module scope.

## 1. The binding (already declared)

`void.config.ts` ships the database; Outer provisions it on first
deploy. Edits need `bun run build` — bindings are read from the built
config:

```ts
d1_databases: [{ binding: "DB", database_name: "app-db" }],
```

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. Tables in code (the starter pattern)

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 where you use them, as the
starter page does:

```ts
import { env } from "cloudflare:workers";

async function database() {
  await env.DB.prepare(
    "CREATE TABLE IF NOT EXISTS item (id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL)",
  ).run();
  return env.DB;
}

export const loader = defineHandler(async () => {
  const db = await database();
  const { results } = await db
    .prepare("SELECT id, text 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:

```ts
await db.prepare("INSERT INTO item (text) VALUES (?)").bind(text).run();
await db.prepare("DELETE FROM item WHERE id = ?").bind(id).run();
```

Seed data counts as page text: no strong profanity in `INSERT`s — 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`:

```ts
// 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(),
});
```

```ts
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 — the deploy never
runs migration files, and the template itself uses raw `prepare`/`bind`.

## Sources

- The starter page (D1 binding in code, loader + intent action):
  <https://github.com/ryuzcorp/outer-template/blob/9571514d8d604be68f74d1ce5bc1cd1705572cae/pages/index.server.ts>
- The binding declaration:
  <https://github.com/ryuzcorp/outer-template/blob/9571514d8d604be68f74d1ce5bc1cd1705572cae/void.config.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/>
