This is the official adapter for running Next.js on Cloudflare Workers instead of Vercel. It handles both App Router and Pages Router, supports ISR and SSG, and critically uses the Node.js runtime, not Edge, which means you get full Node.js APIs and broader Next.js feature support. You access Cloudflare bindings like KV, R2, and D1 through getCloudflareContext() in any route or component. The setup requires specific wrangler.jsonc config with nodejs_compat flags and a self-referencing service binding that must match your worker name. The caching system is flexible but needs configuration, with options for R2, KV, or D1 depending on whether you need ISR, time-based revalidation, or tag-based invalidation.
npx -y skills add null-shot/cloudflare-skills --skill cloudflare-opennext --agent claude-codeInstalls into .claude/skills of the current project.
Deploy Next.js applications to Cloudflare Workers using the @opennextjs/cloudflare adapter with full support for App Router, Pages Router, ISR, SSG, and Cloudflare bindings.
npm create cloudflare@latest -- my-next-app --framework=next --platform=workers
cd my-next-app
npm run dev # Local development with Next.js
npm run preview # Preview in Workers runtime
npm run deploy # Deploy to Cloudflare
# 1. Install dependencies
npm install @opennextjs/cloudflare@latest
npm install --save-dev wrangler@latest
# 2. Create wrangler.jsonc (see Configuration section)
# 3. Create open-next.config.ts
# 4. Update next.config.ts
# 5. Add scripts to package.json
# 6. Deploy
npm run deploy
The @opennextjs/cloudflare adapter:
next build to generate the Next.js build output.open-next/ directory with worker.js entry point_next/static, public)Critical: OpenNext uses Next.js Node.js runtime, NOT the Edge runtime:
// ❌ Remove this - Edge runtime not supported
export const runtime = "edge";
// ✅ Default Node.js runtime - fully supported
// No export needed, this is the default
The Node.js runtime provides:
nodejs_compat flagMinimal configuration for OpenNext:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-nextjs-app",
"main": ".open-next/worker.js",
"compatibility_date": "2024-12-30",
"compatibility_flags": [
"nodejs_compat", // Required for Node.js APIs
"global_fetch_strictly_public" // Security: prevent local IP fetches
],
"assets": {
"directory": ".open-next/assets", // Static files
"binding": "ASSETS"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "my-nextjs-app" // Must match "name" above
}
],
"images": {
"binding": "IMAGES" // Optional: Enable image optimization
}
}
Required settings:
nodejs_compat compatibility flagcompatibility_date >= 2024-09-23WORKER_SELF_REFERENCE service binding (must match worker name)main and assets paths should not be changedSee references/configuration.md for complete configuration with R2, KV, D1 bindings.
Configure caching and OpenNext behavior:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
});
This file is auto-generated if not present. See references/caching.md for cache options.
Initialize OpenNext for local development:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Your Next.js configuration
};
export default nextConfig;
// Enable bindings access during `next dev`
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();
Environment variables for local development:
# .dev.vars
NEXTJS_ENV=development
The NEXTJS_ENV variable selects which Next.js .env file to load:
development → .env.developmentproduction → .env.production (default)Use getCloudflareContext() to access bindings in any route:
import { getCloudflareContext } from "@opennextjs/cloudflare";
// Route Handler (App Router)
export async function GET(request: Request) {
const { env, cf, ctx } = getCloudflareContext();
// Access KV
const value = await env.MY_KV.get("key");
// Access R2
const object = await env.MY_BUCKET.get("file.txt");
// Access D1
const result = await env.DB.prepare("SELECT * FROM users").all();
// Access Durable Objects
const stub = env.MY_DO.idFromName("instance-1");
const doResponse = await stub.fetch(request);
// Access request info
const country = cf?.country;
// Background tasks
ctx.waitUntil(logAnalytics());
return Response.json({ value });
}
// API Route (Pages Router)
export default async function handler(req, res) {
const { env } = getCloudflareContext();
const data = await env.MY_KV.get("key");
res.json({ data });
}
// Server Component
export default async function Page() {
const { env } = getCloudflareContext();
const data = await env.MY_KV.get("key");
return <div>{data}</div>;
}
For Static Site Generation routes, use async mode:
// In SSG route (generateStaticParams, etc.)
const { env } = await getCloudflareContext({ async: true });
const products = await env.DB.prepare("SELECT * FROM products").all();
Warning: During SSG, secrets from .dev.vars and local binding values are included in the static build. Be careful with sensitive data.
Generate types for your bindings:
npx wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts
Add to package.json:
{
"scripts": {
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}
Run after any binding changes in wrangler.jsonc.
The opennextjs-cloudflare CLI wraps Wrangler with OpenNext-specific behavior:
# Build the Next.js app and transform for Workers
npx opennextjs-cloudflare build
# Build and preview locally with Wrangler
npm run preview
# or
npx opennextjs-cloudflare preview
# Build and deploy to Cloudflare
npm run deploy
# or
npx opennextjs-cloudflare deploy
# Build and upload as a version (doesn't deploy)
npm run upload
# or
npx opennextjs-cloudflare upload
# Populate cache (called automatically by preview/deploy/upload)
npx opennextjs-cloudflare populateCache local # Local bindings
npx opennextjs-cloudflare populateCache remote # Remote bindings
Recommended package.json scripts:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}
OpenNext supports Next.js caching with Cloudflare storage:
| Cache Type | Use Case | Storage Options |
|---|---|---|
| Incremental Cache | ISR/SSG page data | R2, KV, Static Assets |
| Queue | Time-based revalidation | Durable Objects, Memory |
| Tag Cache | On-demand revalidation | D1, Durable Objects |
Quick setup examples:
// Static Site (SSG only)
import staticAssetsCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: staticAssetsCache,
enableCacheInterception: true,
});
// Small Site with ISR
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
export default defineCloudflareConfig({
incrementalCache: r2IncrementalCache,
queue: doQueue,
tagCache: d1NextTagCache,
});
See references/caching.md for complete caching patterns including regional cache and sharded tag cache
Enable Cloudflare Images for automatic image optimization:
// wrangler.jsonc
{
"images": {
"binding": "IMAGES"
}
}
Next.js <Image> components will automatically use Cloudflare Images. Additional costs apply.
Compatibility notes:
minimumCacheTTL not supporteddangerouslyAllowLocalIP not supportedCritical Rule: Never create global database clients in Workers. Create per-request:
// ❌ WRONG - Global client causes I/O errors
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// ✅ CORRECT - Per-request client
import { cache } from "react";
import { Pool } from "pg";
export const getDb = cache(() => {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
maxUses: 1, // Don't reuse connections across requests
});
return drizzle({ client: pool, schema });
});
// Usage in route
export async function GET() {
const db = getDb();
const users = await db.select().from(usersTable);
return Response.json(users);
}
See references/database-orm.md for Drizzle and Prisma patterns.
export const runtime = "edge"cache() for request-scoped instancesexport const runtime = "edge" from all routesnext build, not next build --turbo| Feature | Support | Notes |
|---|---|---|
| App Router | ✅ Full | All features supported |
| Pages Router | ✅ Full | Including API routes |
| Route Handlers | ✅ Full | GET, POST, etc. |
| Dynamic Routes | ✅ Full | [slug], [...slug] |
| SSG | ✅ Full | Static Site Generation |
| SSR | ✅ Full | Server-Side Rendering |
| ISR | ✅ Full | Incremental Static Regeneration |
| PPR | ✅ Full | Partial Prerendering |
| Middleware | ✅ Partial | Standard middleware works, Node Middleware (15.2+) not supported |
| Image Optimization | ✅ Full | Via Cloudflare Images binding |
| Composable Caching | ✅ Full | 'use cache' directive |
| next/font | ✅ Full | Font optimization |
| after() | ✅ Full | Background tasks |
| Turbopack | ❌ No | Use standard build |
Supported Next.js versions:
# Local development with Next.js dev server
npm run dev
# Preview in Workers runtime (faster than deploy)
npm run preview
# Deploy to production
npm run deploy
# Update TypeScript types after binding changes
npm run cf-typegen
Local Development Notes:
next dev - Uses Node.js runtime, bindings available via initOpenNextCloudflareForDev()npm run preview - Uses Workers runtime with Wrangler, closer to productionIf migrating from @cloudflare/next-on-pages:
@cloudflare/next-on-pages and eslint-plugin-next-on-pages@opennextjs/cloudflarenext.config.ts:
setupDevPlatform() callsinitOpenNextCloudflareForDev()getRequestContext from @cloudflare/next-on-pagesgetCloudflareContext from @opennextjs/cloudflareexport const runtime = "edge")Official examples in the @opennextjs/cloudflare repository:
create-next-app - Basic Next.js startermiddleware - Middleware usagevercel-blog-starter - SSG blog examplecf-typegen to get binding typesnpm run preview before deployingcache() for per-request instancesobservability to wrangler.jsonc for loggingSee references/configuration.md for complete examples including:
microsoft/azure-skills
zxkane/aws-skills
awslabs/agent-plugins
microck/ordinary-claude-skills
microsoft/github-copilot-for-azure
zxkane/aws-skills