wrangler is Cloudflare’s official CLI tool for managing Cloudflare Workers.
Creating a Local D1 Database
After configuring d1_databases in wrangler.toml or wrangler.jsonc, you
may want to create a local SQLite database to simulate D1.
d1_databases = [
{ binding = "DB", database_name = "my_db", database_id = "1234567890" }
]By default, wrangler doesn’t create this local database immediately. To create it:
- Run
npx wrangler dev - Execute a SQL query, e.g.,
npx wrangler d1 execute my_db --local --command='SELECT * FROM my_table'
Then you’ll see the SQLite database created under .wrangler/state/v3/d1/:
.wrangler
├── state
│ └── v3
│ ├── cache
│ │ └── miniflare-CacheObject
│ ├── d1
│ │ └── miniflare-D1DatabaseObject
│ │ ├── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite
│ │ ├── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite-shm
│ │ └── 14c8722131cce17d31bc37d958e4eacf978b0d4d2c28dea6784e37418eeb3643.sqlite-wal
│ └── workflows
└── tmpYou can then configure a drizzle-dev.config.ts pointing to this local SQLite
database for local CRUD operations.
D1 + Drizzle ORM Setup (The Wrong Way)
Drizzle ORM can provide a config file per environment. Typically you’d configure development and production environments.
Example drizzle-dev.config.ts:
import { defineConfig } from "drizzle-kit";
import fs from "fs";
import path from "path";
function resolveDbPath() {
const dir = path.resolve(".wrangler/state/v3/d1/miniflare-D1DatabaseObject");
try {
const files = fs.readdirSync(dir);
const sqliteFile = files.find((file) => file.endsWith(".sqlite"));
if (!sqliteFile) {
throw new Error(`No .sqlite file found in ${dir}`);
}
return path.join(dir, sqliteFile);
} catch (err) {
console.error("Error resolving database path:", err);
throw new Error(
"Could not resolve the local D1 database path. Please ensure you have manually created the local db correctly."
);
}
}
export default defineConfig({
schema: "./src/db/schemas/*",
out: "drizzle/migrations",
dialect: "sqlite",
dbCredentials: {
url: resolveDbPath(),
},
});Generate migrations:
npx drizzle-kit generate --config drizzle-dev.config.ts --name='update_tables'Add a convenience script to package.json:
"scripts": {
"db:generate": "drizzle-kit generate --config drizzle-dev.config.ts"
}Usage:
npm run db:generate -- --name='init_tables'D1 + Drizzle ORM Setup (The Right Way)
The drizzle.config.ts file isn’t strictly required — especially at runtime,
it’s not needed.
For Cloudflare Workers projects, the main purpose of drizzle.config.ts locally
is generating migrations files. To apply migrations to D1, use wrangler.
Create drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "sqlite",
schema: "./src/db/schemas",
driver: "d1-http",
out: "./drizzle/migrations",
dbCredentials: {
accountId: process.env.CF_ACCOUNT_ID!,
databaseId: process.env.CF_DATABASE_ID!,
token: process.env.CF_ACCESS_TOKEN!,
},
});The dbCredentials params can be empty in .env.local — drizzle won’t actually
use them since runtime code doesn’t read drizzle.config.ts.
Generate migrations
npx drizzle-kit generate --name=your_messageApply migrations locally
npx wrangler d1 apply $your_database_name --localThis applies migrations to the SQLite database under .wrangler/state/v3/d1/.
Connect at runtime
import { drizzle } from "drizzle-orm/d1";
import { Database } from "../types";
import * as schemas from "./schemas";
export function getDatabase(db: D1Database): Database {
return drizzle(db, { schema: schemas });
}Just provide the D1Database object — this is the D1 Binding declared in your
Workers env. Configuration:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "xxxxx",
"database_id": "xxxxx",
"migrations_dir": "drizzle/migrations"
}
]
}After running npx wrangler typegen, you’ll find the Env definition in
worker-configuration.d.ts:
declare namespace Cloudflare {
interface Env {
CF_ACCOUNT_ID: string;
CF_DATABASE_ID: string;
CF_ACCESS_TOKEN: string;
DB: D1Database;
}
}
interface CloudflareBindings extends Cloudflare.Env {}Service Binding Dispose Pattern
Wrangler 4.x supports implicit dispose via the using keyword. However, you may
encounter errors like Object cannot be disposed.
Check your tsconfig.json:
{
"compilerOptions": {
"target": "ES2017"
}
}With this target, one approach is explicit dispose calls:
import { getCloudflareContext } from "@opennextjs/cloudflare";
export async function runRpcUserService<T>(
callback: (userService: IUserService) => Promise<T>
) {
const { env } = getCloudflareContext();
// @ts-expect-error: ignore type error
const userService = await env.PICKNAMES_CORE.getUserService();
try {
return await callback(userService);
} finally {
userService[Symbol.dispose];
}
}The issue is the as IUserService type cast — these interfaces aren’t
Disposable, causing dispose-related errors.
Per Cloudflare docs on RpcTarget, the correct pattern is:
export async function runRpcUserService<T>(
callback: (userService: IUserService) => Promise<T>
) {
const { env } = getCloudflareContext();
// @ts-expect-error: ignore type error
const userService = await env.PICKNAMES_CORE.getUserService();
try {
return await callback(userService);
} finally {
(await userService)[Symbol.dispose]?.();
}
}
export async function runRpcChannelService<T>(
callback: (channelService: IChannelService) => Promise<T>
) {
const { env } = getCloudflareContext();
// @ts-expect-error: ignore type error
const channelService = await env.PICKNAMES_CORE.getChannelService();
try {
return await callback(channelService);
} catch (e) {
console.error(e);
} finally {
(await channelService)[Symbol.dispose]?.();
}
}This approach:
- Removes type cast issues
- Though RPC returns
any, thecallbackparameter provides concrete types for callers