How to Configure Cloudflare D1 Database Bindings with Next.js on Workers
To configure Cloudflare D1 database bindings with Next.js on Workers, declare the database in wrangler.jsonc, generate TypeScript definitions with cf-typegen, and access the binding at runtime via getCloudflareContext() from @opennextjs/cloudflare.
This guide walks through the exact implementation used in the ifindev/fullstack-next-cloudflare repository, where Next.js runs on Cloudflare Workers using OpenNext. The setup enables type-safe SQLite queries at the edge through Drizzle-ORM.
Understanding the Binding Architecture
Cloudflare Workers receive an env object containing all bound resources. When you deploy a Next.js app to Workers, the runtime injects D1 database instances into this object based on your configuration.
How D1 Bindings Work
The D1 binding creates a named reference that becomes available as env.<binding_name> inside your worker. In wrangler.jsonc, the d1_databases array maps binding names to actual database IDs. At runtime, the env object provided by getCloudflareContext() carries this binding, allowing direct SQL execution without network round-trips to external database servers.
Type-Safe Access Pattern
The repository implements a helper pattern in src/db/index.ts that wraps the raw binding with Drizzle-ORM. This abstraction ensures every database interaction is fully typed while hiding the complexity of context retrieval from your business logic.
Step-by-Step Configuration
1. Create the D1 Database
First, provision the database using the Wrangler CLI:
wrangler d1 create your-app-name
# Copy the generated database_id for the next step
This command returns a UUID that uniquely identifies your database in Cloudflare's infrastructure.
2. Declare the Binding in wrangler.jsonc
Open wrangler.jsonc and add the database to the d1_databases array. The binding property defines the JavaScript identifier you'll use to access the database:
// wrangler.jsonc
{
"name": "next-cf-app",
"main": ".open-next/worker.js",
"compatibility_date": "2025-03-01",
"d1_databases": [
{
"binding": "next_cf_app",
"database_name": "next-cf-app",
"database_id": "757a32d1-5779-4f09-bcf3-b268013395d4",
"migrations_dir": "./src/drizzle"
}
]
}
The migrations_dir field tells Wrangler where to find Drizzle schema files for local development.
3. Generate TypeScript Definitions
Run the type generation command to create compile-time safety for your bindings:
pnpm run cf-typegen
This produces worker-configuration.d.ts, which declares next_cf_app: D1Database (or whichever binding name you chose) on the env interface. Without this step, TypeScript treats the binding as any, eliminating intellisense and type checking.
4. Create the Database Helper
Create or verify src/db/index.ts contains the following factory function:
// src/db/index.ts
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";
export async function getDb() {
const { env } = await getCloudflareContext();
// The property name must match the binding in wrangler.jsonc
return drizzle(env.next_cf_app, { schema });
}
export * from "./schema";
This function must be called from within worker-bound code (API routes, server actions, or React Server Components in the Edge runtime). The getCloudflareContext() call throws if executed outside the Worker environment, such as in standard Node.js tests.
5. Apply Database Migrations
Before querying data, push your schema to the D1 instance:
pnpm run db:generate # Generate migration SQL from schema
pnpm run db:migrate:local # Apply to local D1 during development
Querying D1 in Application Code
With the helper established, any server-side module can access the database by importing getDb():
// src/modules/todos/actions/get-todos.action.ts
import { getDb, todos } from "@/db";
import { eq } from "drizzle-orm";
export async function getTodos(userId: string) {
const db = await getDb();
return db.select()
.from(todos)
.where(eq(todos.user_id, userId))
.all();
}
The env.next_cf_app binding resolves automatically whether running locally via wrangler dev or in production on Cloudflare's edge network.
Local Development Workflow
Cloudflare's local development server automatically provisions a local D1 instance when you run:
pnpm run wrangler:dev
This starts the Worker runtime with the D1 binding available at env.next_cf_app. For the full Next.js experience with Hot Module Replacement, run the standard Next.js dev server alongside it:
# Terminal 1
pnpm run wrangler:dev
# Terminal 2
pnpm run dev
Your application at http://localhost:3000 now reads and writes to the local D1 database through the configured binding.
Troubleshooting Common Configuration Issues
| Issue | Solution |
|---|---|
| Binding name mismatch | Verify the string in env.<binding> inside src/db/index.ts exactly matches the "binding" value in wrangler.jsonc. |
| Missing TypeScript types | If env.next_cf_app shows as any, re-run pnpm run cf-typegen to regenerate worker-configuration.d.ts. |
| Context unavailable | Ensure getDb() is called inside async functions running on the Worker. It cannot run in static generation or pure Node contexts. |
| Database not found | Confirm the database_id in wrangler.jsonc matches the ID returned from wrangler d1 create. |
Summary
- Declare bindings in
wrangler.jsoncusing thed1_databasesarray with a unique binding name and database ID. - Generate types via
pnpm run cf-typegento enable IDE autocomplete and compile-time safety forenvproperties. - Access the binding through
getCloudflareContext()from@opennextjs/cloudflare, available exclusively in Worker runtime contexts. - Abstract the connection in
src/db/index.tsusing Drizzle-ORM to provide clean, type-safe database access across your Next.js application. - Develop locally using
wrangler dev, which automatically creates a local D1 instance bound to your worker.
Frequently Asked Questions
How do I change the D1 database binding name?
Modify the "binding" property in wrangler.jsonc to your preferred identifier (e.g., "DB" or "primary_db"), then update src/db/index.ts to reference the same name on the env object (e.g., env.DB). Run pnpm run cf-typegen afterward to update TypeScript definitions.
Can I use multiple D1 databases in the same Next.js app?
Yes. Add multiple objects to the d1_databases array in wrangler.jsonc, each with a unique binding name. Create separate helper functions or modify getDb() to accept a binding name parameter, passing the appropriate env.<binding> to Drizzle.
Why does getCloudflareContext() throw an error in my tests?
getCloudflareContext() requires the Cloudflare Worker runtime environment. It will throw if called outside a Worker context, such as in Jest tests running in Node.js or during Next.js static site generation. Use integration tests with wrangler dev or mock the context for unit testing.
What is the relationship between wrangler.jsonc and the env object?
wrangler.jsonc serves as the configuration manifest. When Wrangler deploys or runs your worker, it reads this file and injects the specified resources into the env object available at runtime. The binding field in the JSON becomes the property name on env that holds the actual D1Database instance.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →