D1 Setup
The application expects a Cloudflare D1 binding named DB, and
Drizzle writes migrations to drizzle/d1. The database schema lives in
src/db/schema, with src/db/schema/index.ts as the Drizzle Kit entrypoint.
D1 is currently the only supported runtime database. Runtime code should use the local
src/db modules rather than importing drizzle-orm/d1 directly from routes,
actions, middleware, or UI code.
Hyperdrive support is planned for future releases. The project has proof tests for future Hyperdrive PostgreSQL and MySQL support, but these targets are not enabled at runtime. See Hyperdrive Proof for the current adapter proof-of-concept work on the database boundary.
Default Setup
The committed default is a single D1 binding in wrangler.jsonc:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "vk",
"database_id": "00000000-0000-0000-0000-000000000000",
"migrations_dir": "drizzle/d1",
},
],
}Runtime code always reads env.DB; keep that binding name unless you also update
src/env.d.ts and every env.DB call site.
Local development uses the Astro Cloudflare adapter and Wrangler config. The
npm run dev script runs astro dev, and the adapter reads wrangler.jsonc so
the DB binding is available through the local Workers runtime. Wrangler's local
D1 implementation is powered by Miniflare/workerd. No separate Miniflare config
is required for the default flow.
Wrangler is already a project dev dependency. Install dependencies with
npm install, then use the project scripts. If you want to call Wrangler
directly, prefer npx wrangler ... so the repository-pinned version is used.
Local Development
Install dependencies first:
npm installGenerate migrations after schema changes:
npm run db:generateApply migrations to Wrangler's local D1 state:
npm run db:migrate:localOptionally create a verified local user with the admin role after migrations:
npm run init:adminThis writes directly to D1 with Wrangler and does not require npm run dev.
Wrangler stores local D1 state under .wrangler/, which is ignored by git.
The Miniflare-backed D1 SQLite files live at:
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/The application database is the hashed *.sqlite file in that directory. The
metadata.sqlite files are Miniflare bookkeeping and are not the app database.
Wrangler persists local D1 data between dev sessions by default. If you need a
fresh local database, remove the relevant .wrangler state or reset it with SQL.
Run the app:
npm run devThe app uses the same DB binding locally, but the data is local-only unless the
binding is configured as remote.
Drizzle Studio
The project includes:
npm run db:studioDrizzle Studio reads drizzle.config.ts. With this project's default
driver: 'd1-http' config, Studio connects through Cloudflare's D1 HTTP API,
not through Wrangler's local DB binding. Use this mode for a Cloudflare-hosted
D1 database by setting the required values in the shell that starts Studio:
CLOUDFLARE_ACCOUNT_ID=your-account-id \
CLOUDFLARE_DATABASE_ID=your-d1-database-id \
CLOUDFLARE_D1_TOKEN=your-api-token \
npm run db:studioDo not put CLOUDFLARE_D1_TOKEN in wrangler.jsonc or commit it. Drizzle Kit
does not read the Wrangler login session for this path; it needs the explicit
Cloudflare account, database, and token values.
If you run npm run db:studio without real Cloudflare values, Drizzle Kit will
use the placeholder local values from drizzle.config.ts and eventually fail
with a Cloudflare routing error such as
Could not route to /client/v4/accounts/local/d1/database/local/query. That
means Studio is trying to use remote D1 mode without real remote D1 credentials.
For local D1, Wrangler stores the Miniflare-backed database as a SQLite file
under .wrangler/state/v3/d1/miniflare-D1DatabaseObject/ after local migrations
or local app usage. Drizzle Studio can inspect that file as SQLite, but that is a
local-dev convenience rather than a Cloudflare D1 API connection. This project
does not commit a local Studio config because Studio support is optional; copy
the config below only when you want this workflow.
First apply local migrations:
npm run db:migrate:localThen create an uncommitted local config:
// drizzle.studio.local.config.ts
import { existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { defineConfig } from 'drizzle-kit';
const localD1Directory = '.wrangler/state/v3/d1/miniflare-D1DatabaseObject';
function toFileUrl(path: string) {
return path.startsWith('file:') ? path : `file:${path}`;
}
function getLocalD1SqliteUrl() {
const explicitPath = process.env.LOCAL_D1_SQLITE_PATH;
if (explicitPath) {
return toFileUrl(explicitPath);
}
if (!existsSync(localD1Directory)) {
throw new Error(
[
`Local D1 state was not found at ${localD1Directory}.`,
'Run npm run db:migrate:local first, or set LOCAL_D1_SQLITE_PATH.',
].join('\n'),
);
}
const sqliteFiles = readdirSync(localD1Directory)
.filter((file) => file.endsWith('.sqlite') && file !== 'metadata.sqlite')
.sort();
if (sqliteFiles.length !== 1) {
throw new Error(
[
`Expected one local D1 SQLite database in ${localD1Directory}.`,
...sqliteFiles.map((file) => `- ${join(localD1Directory, file)}`),
'Set LOCAL_D1_SQLITE_PATH to the database file you want Studio to open.',
].join('\n'),
);
}
return toFileUrl(join(localD1Directory, sqliteFiles[0]));
}
export default defineConfig({
dialect: 'sqlite',
schema: './src/db/schema/index.ts',
dbCredentials: {
url: getLocalD1SqliteUrl(),
},
});Run Studio with that config:
npx drizzle-kit studio --config drizzle.studio.local.config.tsIf you have more than one local D1 file, point Studio at the one you want:
LOCAL_D1_SQLITE_PATH=.wrangler/state/v3/d1/miniflare-D1DatabaseObject/your-file.sqlite \
npx drizzle-kit studio --config drizzle.studio.local.config.tsDrizzle Kit needs either better-sqlite3 or @libsql/client installed to open a
raw SQLite file. This project does not require either package for normal D1
runtime behavior, so install one only if you want the local-file Studio workflow.
The Chrome extension is not required for the CLI workflow above. Drizzle's Chrome extension is an optional way to use Studio from supported browser-based database dashboards. For this project, local D1 inspection should use the CLI Studio server plus the local SQLite file, and Cloudflare-hosted D1 inspection should use the CLI Studio server plus D1 HTTP credentials.
Production Database
Create the remote D1 database once:
npx wrangler d1 create vkCopy the returned database_id into wrangler.jsonc under the DB binding:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "vk",
"database_id": "replace-with-cloudflare-d1-id",
"migrations_dir": "drizzle/d1",
},
],
}For remote migrations, prefer Wrangler's D1 commands:
npm run db:migrate:remoteWrangler reads the D1 binding from wrangler.jsonc and uses your Wrangler auth
session or CLOUDFLARE_API_TOKEN from the shell or CI environment.
If you run a Drizzle Kit command that connects directly to Cloudflare D1 over HTTP, provide these values through your shell or CI secret store instead of a project environment file:
CLOUDFLARE_ACCOUNT_ID=
CLOUDFLARE_DATABASE_ID=
CLOUDFLARE_D1_TOKEN=Apply migrations to the remote database:
npm run db:migrate:remoteOptionally create a verified remote user with the admin role after migrations:
npm run init:admin -- --remoteThis writes directly to the remote D1 database with Wrangler and does not require the app server to be running.
Alternate Development Databases
You have three common options when DB should point somewhere other than the
default local D1 state.
Separate Local D1 State
Keep binding as DB, keep database_name as the database that Wrangler
commands should target, and add a preview_database_id to isolate the local dev
database identity:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "vk",
"database_id": "replace-with-production-cloudflare-d1-id",
"preview_database_id": "vk-local-jane",
"migrations_dir": "drizzle/d1",
},
],
}Then apply migrations to that local database:
npm run db:migrate:localUse a stable preview_database_id if you want local data to persist across
sessions. Use a different preview_database_id when you intentionally want a
separate local D1 database.
Cloudflare-Hosted Dev Database
Create a second Cloudflare D1 database for development:
npx wrangler d1 create vk-devUse a named Wrangler environment so production and development bindings stay
separate. Binding configuration is environment-specific, so repeat both vars
and d1_databases inside the named environment:
{
"env": {
"development": {
"name": "vk-development",
"vars": {
"EMAIL_PROVIDER": "console",
},
"d1_databases": [
{
"binding": "DB",
"database_name": "vk-dev",
"database_id": "replace-with-dev-cloudflare-d1-id",
"migrations_dir": "drizzle/d1",
"remote": true,
},
],
},
},
}Run local dev against that Cloudflare-hosted database by selecting the environment for the Cloudflare Vite runtime:
CLOUDFLARE_ENV=development npm run devApply migrations to that remote dev database with:
npx wrangler d1 migrations apply vk-dev --remote --env developmentremote: true means local requests can mutate the Cloudflare D1 database. Use a
dev database, not production, unless that is intentional.
Production-Like Named Environment
For production, use the same environment pattern but point DB at the production
database and omit remote: true; deploy commands run on Cloudflare and use the
bound remote database there:
{
"env": {
"production": {
"name": "vk-production",
"vars": {
"EMAIL_PROVIDER": "console",
},
"d1_databases": [
{
"binding": "DB",
"database_name": "vk",
"database_id": "replace-with-production-cloudflare-d1-id",
"migrations_dir": "drizzle/d1",
},
],
},
},
}Apply migrations and deploy with the same environment name:
npx wrangler d1 migrations apply vk --remote --env production
npx wrangler deploy --env productionRuntime Boundary
Runtime code should continue to use the local src/db modules. Do not import
drizzle-orm/d1 from app routes, actions, middleware, or UI code. Hyperdrive is
a planned adapter target, but D1 is the only supported runtime database in this
milestone.
The app_settings table is part of this app-owned D1 schema. It stores small
key/value settings such as site.title through the local app settings action
surface. It is not a database administration table and should not be used for
migrations, connection management, or database target selection.