---
title: Store data in D1
description: D1 on Outer — the binding, migration order, query API, console grid, and backup summary.
---

The template ships with D1 on by default. Copy `pages/index.server.ts`: a loader that reads D1 and one action (an `intent` field tells forms apart) that writes it. The person-facing guide is [Save app data in a database](/backend/database).

## Use the declared binding

`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" }],
```

One D1 database and one R2 bucket per app: a second `d1_databases` entry fails the deploy at validation ("Outer apps can have one D1 database and one R2 bucket for now; merge the extra ones"). The built `dist/wrangler.json` carries the binding 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.

## Ship schema as migrations

1. Iterate locally with `void db push` (applies the schema with no migration files).
2. Run `void db generate` to write `db/migrations/NNNN_name.sql`.
3. Commit the files; Outer applies them on deploy after the moderation gate, before new code serves traffic.
4. `void db migrate` applies pending migrations to the local database only, never production.

Migration order depends on deploy state:

| Deploy | Order |
| --- | --- |
| App with a live version | Migrations run before the upload; the old code keeps serving, and a migrate failure uploads nothing |
| First deploy | Upload first, then migrate; the proxy returns 503 (building page) until the first deploy succeeds, so no race is possible |

Files apply in numeric-prefix order under the app's scoped storage credentials. Already-applied files are skipped, never re-applied. A file edited after it was applied fails the deploy. Write a new migration instead. A failing migration fails the deploy and names the file; the previous version keeps serving (first deploys stay not-live, "Your app isn't live yet"), with celld's error in the log tail. The step runs only when the built config declares `d1_databases` and the repo holds migration files; no files means the step is skipped.

Expand first, contract later: the migration runs while the old code still serves, so prefer additive changes (new tables, nullable columns). Dropping a column the live code still reads serves errors until the new version takes over.

Seed `INSERT`s count as page text: no strong profanity in seeds. The pre-live gate reads SQL string literals and a profane seed blocks the deploy.

## Query with prepare and bind

`prepare()` takes exactly one statement. `bind(...)` fills `?` placeholders. `all()` returns `{ results }`, `first()` returns 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();
```

Apps with no migration files skip the migrate step, so creating tables where you use them keeps working for tiny apps (`CREATE TABLE IF NOT EXISTS`, as the starter does).

Drizzle is optional: define tables in `db/schema.ts` with `void/schema-d1`, import them through the `@schema` alias, and query through `db` from `void/db`:

```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" });
```

## Browse data in Console

Open the app in Console and go to **Data → D1**. The editor opens the app's single `d1_databases` binding directly, then its tables with row counts. Each table opens a server-paged grid: click a header to sort, use search and the filter popover to narrow rows, and flip to **Definition** for columns, indexes, foreign keys, and the `CREATE` statement. Click a row to update or delete it; **Insert** adds one. Every field is raw text with a NULL toggle, so `NULL` and `""` stay distinct. Checkbox-select rows for bulk delete (up to 100 rows in one atomic batch). There is deliberately no SQL console: every write is a curated insert/update/delete built from the table's schema, each recorded in the app's action log. The owner can read and write.

## Back up the database

Apps with a database get automatic daily dumps once active in the prior 24 hours, kept 7 days (30 on Pro). Console offers back up now, download, and restore; MCP offers `list_backups`, `backup_database`, and `restore_backup` (all owner-only). Full detail lives at [Backups](/agents/backend/backups).
