D1 (SQLite)
Cloudflare D1 is Void's default SQLite database. Start with a schema; 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:
void db generate
void db migrate # apply locally
void deploy # apply remotely and deploySee Database commands for the full workflow.
Batch Operations
D1 supports batching multiple statements into a single round-trip:
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 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' }):
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
To make a read after a save see that save in a later request, carry a D1 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:
// 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:
// 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
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
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.
⚠️ 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.