Cloudflare D1 is a great fit for full-stack Workers projects. Drizzle ORM is a great fit for type-safe SQL in TypeScript. On paper they belong together — but when you try to wire up Drizzle Kit for migrations against a local D1 database, you quickly discover there's a gap nobody has cleanly filled.
This article covers what that gap is, why it exists, and how @deox/drizzle-d1-utils closes it.
How Drizzle Kit Supports D1 — and Where It Stops
Drizzle Kit has a d1-http driver. You configure it in drizzle.config.ts like this:
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
databaseId: 'your-database-id',
accountId: process.env.CF_ACCOUNT_ID!,
token: process.env.CF_API_TOKEN!,
},
schema: './src/schema.ts',
out: './drizzle',
} satisfies Config;
This works perfectly for remote — it hits the Cloudflare D1 HTTP API and runs your migrations against the live database. But for local development, this is the wrong tool. You don't want to run migrations against production every time you iterate on a schema.
The correct approach for local development is to use the SQLite file that Wrangler persists on disk when you run your Worker locally. Drizzle Kit can talk to a plain SQLite file using dbCredentials.url. But Drizzle Kit has no idea where Wrangler put that file — and Wrangler doesn't tell you in any structured way.
The Naive Solution: Find the SQLite File Yourself
Wrangler persists local D1 state somewhere inside .wrangler/state/. The exact path looks like:
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/<hash>.sqlite
The hash in the filename is derived from the binding's database ID, so it isn't predictable. The folder can contain multiple .sqlite files if you have multiple D1 bindings.
The obvious workaround is to search for it:
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
import { globSync } from 'glob';
const [sqliteFile] = globSync('.wrangler/**/*.sqlite');
export default {
dialect: 'sqlite',
dbCredentials: { url: `file:${sqliteFile}` },
schema: './src/schema.ts',
out: './drizzle',
} satisfies Config;
This works — until it doesn't. The moment you have more than one D1 binding, globSync returns multiple files and you're picking the first one arbitrarily. You have no way to know which .sqlite file belongs to which binding without reading the Wrangler config and correlating database IDs to filenames yourself.
Projects with even modest complexity — a primary DB plus a queue, a cache, or an analytics store — hit this immediately. And the solution doesn't scale: you'd need a different drizzle.config.ts per binding, all with hardcoded paths, all fragile to Wrangler version bumps that change the state directory structure.
The Right Approach: Read the Wrangler Config
The information needed to do this correctly is all in wrangler.toml (or wrangler.json). Each D1 binding has:
- A
bindingname (e.g.DB) - A
database_id(used to derive the SQLite filename) - Optionally a
preview_database_id - Optionally a
database_name
Wrangler uses the database_id to construct the SQLite filename deterministically. So if you parse the Wrangler config, extract the right binding by name, and reconstruct the path using the same logic Wrangler uses, you get the exact file — no guessing, no globbing.
That's the core of what @deox/drizzle-d1-utils does. The getD1BindingInfo utility reads your Wrangler config, resolves the correct binding (by name, or defaulting to the first one found), applies the active environment if specified, and returns the fully resolved SQLite path alongside the database metadata.
What the Package Provides
The public API is a single function:
function drizzleD1Config(config: DrizzleKitConfig, options?: DrizzleD1Options): Config
You pass your base Drizzle Kit config (schema path, migrations output directory, etc.) and a set of D1-specific options. The function resolves everything and returns a complete Config ready to export.
Local Mode
// drizzle.config.ts
import { drizzleD1Config } from '@deox/drizzle-d1-utils';
export default drizzleD1Config(
{ schema: './src/schema.ts', out: './drizzle' },
{ binding: 'DB' },
);
That's it. The function reads wrangler.toml, finds the DB binding, derives the SQLite file path from the database_id, and produces:
{
dialect: 'sqlite',
dbCredentials: { url: 'file:.wrangler/state/v3/d1/miniflare-D1DatabaseObject/<hash>.sqlite' },
schema: './src/schema.ts',
out: './drizzle',
}
Remote Mode
// drizzle.config.ts
import { drizzleD1Config } from '@deox/drizzle-d1-utils';
export default drizzleD1Config(
{ schema: './src/schema.ts', out: './drizzle' },
{
binding: 'DB',
remote: true,
accountId: process.env.CF_ACCOUNT_ID,
apiToken: process.env.CF_API_TOKEN,
},
);
In remote mode the function switches to driver: 'd1-http' and forwards the credentials. The remote flag can also be set on the binding in wrangler.toml, in which case you don't need to pass it here at all — the function inherits it from the binding config.
Preview Mode
Wrangler supports a preview_database_id on D1 bindings — a separate database used for preview deployments. Passing preview: true targets that database instead of the production one, in either local or remote mode.
// drizzle.config.ts
import { drizzleD1Config } from '@deox/drizzle-d1-utils';
export default drizzleD1Config(
{ schema: './src/schema.ts', out: './drizzle' },
{ binding: 'DB', preview: true },
);
Environments
If your wrangler.toml uses [env.staging] or similar environment blocks, pass the environment name and the correct binding config for that environment is used:
// drizzle.config.ts
import { drizzleD1Config } from '@deox/drizzle-d1-utils';
export default drizzleD1Config(
{ schema: './src/schema.ts', out: './drizzle' },
{ binding: 'DB', environment: 'staging' },
);
Handling the Missing SQLite File
There's one more edge case that trips people up early in a project: the local SQLite file doesn't exist until you've run your Worker locally at least once (or explicitly initialised the D1 database with Wrangler).
When the file is missing, the package detects it and prompts you interactively:
[!] SQLite file for D1 binding 'DB' does not exist.
[!] The file can be created by executing the following command:
wrangler d1 execute MyDatabase --command='SELECT 1' --local
[!] Run this command to create it? (y/N)
Answering y runs the Wrangler command in-process and then verifies the file was created before continuing. This turns a confusing silent failure into a self-healing setup step.
Installation
npm install -D @deox/drizzle-d1-utils
Peer dependencies:
npm install -D drizzle-kit wrangler
For local mode, a SQLite driver is also required:
npm install -D better-sqlite3
# or
npm install -D @libsql/client
The remote driver (d1-http) is bundled with Drizzle Kit and needs no additional installation.
Why Not Just Use an Environment Variable?
A common workaround is to set a DATABASE_URL environment variable to the SQLite file path and read it in drizzle.config.ts. This works, but it moves the problem rather than solving it — you still need to find the right path, you still need to handle multiple bindings, and you need to keep the variable in sync when Wrangler's state directory changes (which it has between major versions).
Tying the config to the Wrangler config file means the source of truth is always wrangler.toml, which is already the canonical definition of your bindings. When you rename a binding, change a database_id, or add an environment, the config adapts automatically.
Summary
Drizzle Kit's d1-http driver covers the remote case well. The local case — pointing Drizzle Kit at the SQLite file Wrangler persists on disk — has no official solution, and the naive glob-based workaround breaks as soon as a project has more than one D1 binding.
@deox/drizzle-d1-utils solves this by reading the Wrangler config directly, resolving the correct binding by name, and constructing the exact SQLite path the same way Wrangler does. It handles local, remote, and preview modes through a single unified drizzleD1Config function, and it covers the missing-file edge case with an interactive setup prompt.
The result is a drizzle.config.ts that stays clean across environments, doesn't require manual path management, and works correctly regardless of how many D1 bindings your project has.
Documentation Links
@deox/drizzle-d1-utilsGitHub: https://github.com/kumardeo/deox/tree/main/packages/drizzle-d1-utils- Drizzle ORM Docs: https://orm.drizzle.team/docs/overview
Copyright (c):
fineshopdesign.com