---
url: https://void.cloud/guide/database/d1.md
description: >-
  Use Cloudflare D1, Void's default SQLite database. Void runs it locally and
  provisions it when you deploy.
---

# D1 (SQLite)

[Cloudflare D1](https://developers.cloudflare.com/d1/) is Void's default SQLite database. Start with a [schema](../database.md#schema-definition); Void manages the database locally and when you deploy.

## Local Database

The dev server and local `void db` commands share the same database. If `cloudflare.d1_databases` in `void.config.ts` declares the D1 binding, Void uses its ID. A `preview_database_id` takes precedence during local development.

To store local data outside `.void`, set `voidPlugin({ persistTo: 'local-state' })` in your Vite config. Start the dev server once to record that directory before running database commands. After changing the Vite config, start it again to refresh it. Frameworks that manage their own Cloudflare runtime use `.wrangler/state`.

## Migrations & Prototyping

Use `void db push` to apply schema changes locally without creating migration files. Before deploying, generate and commit migrations:

```sh
void db generate
void db migrate # apply locally
void deploy     # apply remotely and deploy
```

See [Database commands](../../reference/cli/database.md#database) for the full workflow.

## Batch Operations

D1 supports batching multiple statements into a single round-trip:

```ts
import { users, posts } from '@schema';
import { db, eq } from 'void/db';

await db.batch([
  db.delete(posts).where(eq(posts.userId, userId)),
  db.delete(users).where(eq(users.id, userId)),
]);
```

## Read replication

Void enables [D1 read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/) when it provisions a database. To enable it on an existing database, run `void deploy --platform cloudflare --provision`. Queries through `db` from `void/db` use a D1 session for each Void request, so reads can run near the request and later queries in that request see at least the same data. Framework handlers without Void's request scope start each database operation on the primary. `createDb` starts a replica-capable session for the binding you pass it.

Replicas can lag between requests. Authentication session checks start on the primary so a revoked session is not accepted because of replica lag. For another read that needs the latest primary data, create a Drizzle client inside that request with `createDb(env.DB, { session: 'first-primary' })`:

```ts
import { users } from '@schema';
import { env } from 'cloudflare:workers';
import { defineHandler } from 'void';
import { createDb } from 'void/db';

export const GET = defineHandler(async () => {
  const primaryDb = createDb(env.DB, { session: 'first-primary' });
  return primaryDb.select().from(users).all();
});
```

The first query uses the primary; subsequent queries in that session see at least the same data. Without this option, `createDb()` uses `'first-unconstrained'`, allowing its first query to use a replica. Other query libraries and raw D1 calls need their own `withSession()` to use replicas; a bare D1 binding reads from the primary.

### Carrying bookmarks between requests {#cross-request-bookmarks}

To make a read after a save see that save in a later request, carry a [D1 bookmark](https://developers.cloudflare.com/d1/best-practices/read-replication/#start-a-session-from-previous-context-bookmark) between requests. A bookmark allows D1 to use a replica that has caught up to at least the previously observed database state.

Use middleware to set the default `db` session before your handlers or Pages loaders run, and return its bookmark after their queries. For a browser app, a cookie carries it through redirects and navigation:

```ts
// middleware/01.database.ts
import { getCookie, setCookie } from 'hono/cookie';
import { defineMiddleware } from 'void';
import { getBookmark, setSession } from 'void/db';

export default defineMiddleware(async (c, next) => {
  setSession(getCookie(c, 'db-bookmark') ?? 'first-unconstrained');
  await next();

  const bookmark = getBookmark();
  if (bookmark) {
    setCookie(c, 'db-bookmark', bookmark, {
      httpOnly: true,
      secure: new URL(c.req.url).protocol === 'https:',
      sameSite: 'Lax',
      path: '/',
    });
  }
  c.header('Cache-Control', 'private, no-store');
});
```

Your handlers and loaders keep importing `db` from `void/db`. No cookie is created automatically. This example uses a session cookie; if you add an expiry, the freshness guarantee from that bookmark ends when it expires. Parallel responses can overwrite the cookie out of order, so this simple transport does not guarantee monotonic bookmarks across concurrent requests.

For API clients, exchange a header instead:

```ts
// middleware/01.database.ts
import { defineMiddleware } from 'void';
import { getBookmark, setSession } from 'void/db';

export default defineMiddleware(async (c, next) => {
  setSession(c.req.header('X-DB-Bookmark') ?? 'first-unconstrained');
  await next();
  const bookmark = getBookmark();
  if (bookmark) {
    c.header('X-DB-Bookmark', bookmark);
  }
  c.header('Cache-Control', 'private, no-store');
});
```

The client must save the returned header and send it on its next request. For a separate D1 binding or a framework handler outside Void's request scope, pass the bookmark to `createDb(binding, { session: bookmark })` and read the resulting bookmark with `getBookmark(client)`.

Bookmarks constrain database queries, not cached responses. Disable ISR for pages that require this guarantee (`export const revalidate = 0` in the page's `.server.ts` file), and do not serve those requests from shared response caches. A bookmark also does not guarantee visibility of a later write from another client, such as an independent payment webhook; use a primary-first session when you need the latest primary data.

## Alternative Query Layers

Use `env.DB` from `cloudflare:workers` with another query library or the raw D1 API:

### Kysely

```ts
import { env } from 'cloudflare:workers';
import { Kysely } from 'kysely';
import { D1Dialect } from 'kysely-d1';

const db = new Kysely<Database>({ dialect: new D1Dialect({ database: env.DB }) });
```

### Raw D1

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

const { results } = await env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).all();
```

Void still handles binding inference and deployment regardless of which query layer you use.

::: warning ⚠️ Meta framework limitation
Nuxt and SvelteKit 2 do not support `cloudflare:workers` imports during local development. Use `void/db` or your framework's binding access API. SvelteKit 3 supports these imports.
:::
