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.jsonc using the d1_databases array with a unique binding name and database ID.
  • Generate types via pnpm run cf-typegen to enable IDE autocomplete and compile-time safety for env properties.
  • Access the binding through getCloudflareContext() from @opennextjs/cloudflare, available exclusively in Worker runtime contexts.
  • Abstract the connection in src/db/index.ts using 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →