cloudflare-nextjs
Cloudflare Next.js Deployment Skill
Deploy Next.js applications to Cloudflare Workers using the OpenNext Cloudflare adapter for production-ready serverless Next.js hosting.
Use This Skill When
- Deploying Next.js applications (App Router or Pages Router) to Cloudflare Workers
- Need server-side rendering (SSR), static site generation (SSG), or incremental static regeneration (ISR) on Cloudflare
- Migrating existing Next.js apps from Vercel, AWS, or other platforms to Cloudflare
- Building full-stack Next.js applications with Cloudflare services (D1, R2, KV, Workers AI)
- Need React Server Components, Server Actions, or Next.js middleware on Workers
- Want global edge deployment with Cloudflare's network
Key Concepts
OpenNext Adapter Architecture
The OpenNext Cloudflare adapter (@opennextjs/cloudflare) transforms Next.js build output into Cloudflare Worker-compatible format. This is fundamentally different from standard Next.js deployments:
- Node.js Runtime Required: Uses Node.js runtime in Workers (NOT Edge runtime)
- Dual Development Workflow: Test in both Next.js dev server AND workerd runtime
- Custom Build Pipeline:
next build→ OpenNext transformation → Worker deployment - Cloudflare-Specific Configuration: Requires wrangler.jsonc and open-next.config.ts
Critical Differences from Standard Next.js
| Aspect | Standard Next.js | Cloudflare Workers |
|---|---|---|
| Runtime | Node.js or Edge | Node.js (via nodejs_compat) |
| Dev Server | next dev |
next dev + opennextjs-cloudflare preview |
| Deployment | Platform-specific | opennextjs-cloudflare deploy |
| Worker Size | No limit | 3 MiB (free) / 10 MiB (paid) |
| Database Connections | Global clients OK | Must be request-scoped |
| Image Optimization | Built-in | Via Cloudflare Images |
| Caching | Next.js cache | OpenNext config + Workers cache |
Setup Patterns
New Project Setup
Use Cloudflare's create-cloudflare (C3) CLI to scaffold a new Next.js project pre-configured for Workers:
npm create cloudflare@latest -- my-next-app --framework=next
What this does:
- Runs Next.js official setup tool (
create-next-app) - Installs
@opennextjs/cloudflareadapter - Creates
wrangler.jsoncwith correct configuration - Creates
open-next.config.tsfor caching configuration - Adds deployment scripts to
package.json - Optionally deploys immediately to Cloudflare
Development workflow:
npm run dev # Next.js dev server (fast reloads)
npm run preview # Test in workerd runtime (production-like)
npm run deploy # Build and deploy to Cloudflare
Existing Project Migration
To add the OpenNext adapter to an existing Next.js application:
1. Install the adapter
npm install --save-dev @opennextjs/cloudflare
2. Create wrangler.jsonc
{
"name": "my-next-app",
"compatibility_date": "2025-05-05",
"compatibility_flags": ["nodejs_compat"]
}
Critical configuration:
compatibility_date: Minimum2025-05-05(for FinalizationRegistry support)compatibility_flags: Must includenodejs_compat(for Node.js runtime)
3. Create open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({
// Caching configuration (optional)
// See: https://opennext.js.org/cloudflare/caching
});
4. Update package.json scripts
{
"scripts": {
"dev": "next dev",
"build": "next build",
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}
Script purposes:
dev: Next.js development server (fast iteration)preview: Build + run in workerd runtime (test before deploy)deploy: Build + deploy to Cloudflarecf-typegen: Generate TypeScript types for Cloudflare bindings
5. Ensure Node.js runtime (not Edge)
Remove Edge runtime exports from your app:
// ❌ REMOVE THIS (Edge runtime not supported)
export const runtime = "edge";
// ✅ Use Node.js runtime (default)
// No export needed - Node.js is default
Development Workflow
Dual Testing Strategy
Always test in BOTH environments:
-
Next.js Dev Server (
npm run dev)- Fast hot reloading
- Best developer experience
- Runs in Node.js (not production runtime)
- Use for rapid iteration
-
Workerd Runtime (
npm run preview)- Runs in production-like environment
- Catches runtime-specific issues
- Slower rebuild times
- Required before deployment
When to Use Each
# Iterating on UI/logic → Use Next.js dev server
npm run dev
# Testing integrations (D1, R2, KV) → Use preview
npm run preview
# Before deploying → ALWAYS test preview
npm run preview
# Deploy to production
npm run deploy
Configuration Requirements
Wrangler Configuration
Minimum requirements in wrangler.jsonc:
{
"name": "your-app-name",
"compatibility_date": "2025-05-05", // Minimum for FinalizationRegistry
"compatibility_flags": ["nodejs_compat"] // Required for Node.js runtime
}
Environment Variables for Package Exports
If using npm packages with multiple export conditions, create .env:
WRANGLER_BUILD_CONDITIONS=""
WRANGLER_BUILD_PLATFORM="node"
This ensures Wrangler prioritizes the node export when available.
Cloudflare Bindings Integration
Add bindings in wrangler.jsonc:
{
"name": "your-app-name",
"compatibility_date": "2025-05-05",
"compatibility_flags": ["nodejs_compat"],
// D1 Database
"d1_databases": [
{
"binding": "DB",
"database_name": "production-db",
"database_id": "your-database-id"
}
],
// R2 Storage
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "your-bucket"
}
],
// KV Storage
"kv_namespaces": [
{
"binding": "KV",
"id": "your-kv-id"
}
],
// Workers AI
"ai": {
"binding": "AI"
}
}
Access bindings in Next.js via process.env:
// app/api/route.ts
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
// Access Cloudflare bindings
const env = process.env as any;
// D1 Database query
const result = await env.DB.prepare('SELECT * FROM users').all();
// R2 Storage access
const file = await env.BUCKET.get('file.txt');
// KV Storage access
const value = await env.KV.get('key');
// Workers AI inference
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt: 'Hello AI'
});
return Response.json({ result });
}
Error Prevention (10+ Documented Errors)
1. Worker Size Limit Exceeded (3 MiB - Free Plan)
Error: "Your Worker exceeded the size limit of 3 MiB"
Cause: Workers Free plan limits Worker size to 3 MiB (gzip-compressed)
Solutions:
- Upgrade to Workers Paid plan (10 MiB limit)
- Analyze bundle size and remove unused dependencies
- Use dynamic imports to code-split large dependencies
Bundle analysis:
npx opennextjs-cloudflare build
cd .open-next/server-functions/default
# Analyze handler.mjs.meta.json with ESBuild Bundle Analyzer
Source: https://opennext.js.org/cloudflare/troubleshooting#worker-size-limits
2. Worker Size Limit Exceeded (10 MiB - Paid Plan)
Error: "Your Worker exceeded the size limit of 10 MiB"
Cause: Unnecessary code bundled into Worker
Debug workflow:
- Run
npx opennextjs-cloudflare build - Navigate to
.open-next/server-functions/default - Analyze
handler.mjs.meta.jsonusing ESBuild Bundle Analyzer - Identify and remove/externalize large dependencies
Source: https://opennext.js.org/cloudflare/troubleshooting#worker-size-limits
3. FinalizationRegistry Not Defined
Error: "ReferenceError: FinalizationRegistry is not defined"
Cause: compatibility_date in wrangler.jsonc is too old
Solution: Update compatibility_date to 2025-05-05 or later:
{
"compatibility_date": "2025-05-05" // Minimum for FinalizationRegistry
}
Source: https://opennext.js.org/cloudflare/troubleshooting#finalizationregistry-is-not-defined
4. Cannot Perform I/O on Behalf of Different Request
Error: "Cannot perform I/O on behalf of a different request"
Cause: Database client created globally and reused across requests
Problem code:
// ❌ WRONG: Global DB client
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export async function GET() {
// This will fail - pool created in different request context
const result = await pool.query('SELECT * FROM users');
return Response.json(result);
}
Solution: Create database clients inside request handlers:
// ✅ CORRECT: Request-scoped DB client
import { Pool } from 'pg';
export async function GET() {
// Create client within request context
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query('SELECT * FROM users');
await pool.end();
return Response.json(result);
}
Alternative: Use Cloudflare D1 (designed for Workers) instead of external databases:
// ✅ BEST: Use D1 (no connection pooling needed)
export async function GET(request: NextRequest) {
const env = process.env as any;
const result = await env.DB.prepare('SELECT * FROM users').all();
return Response.json(result);
}
5. NPM Package Import Failures
Error: "Could not resolve '<package>'"
Cause: Missing nodejs_compat flag or package export conditions
Solution 1: Enable nodejs_compat flag:
{
"compatibility_flags": ["nodejs_compat"]
}
Solution 2: For packages with multiple exports, create .env:
WRANGLER_BUILD_CONDITIONS=""
WRANGLER_BUILD_PLATFORM="node"
Source: https://opennext.js.org/cloudflare/troubleshooting#npm-packages-fail-to-import
6. Failed to Load Chunk (Turbopack)
Error: "Failed to load chunk server/chunks/ssr/"
Cause: Next.js built with Turbopack (next build --turbo)
Solution: Use standard build (Turbopack not supported by adapter):
{
"scripts": {
"build": "next build" // ✅ Correct
// "build": "next build --turbo" // ❌ Don't use Turbopack
}
}
Source: https://opennext.js.org/cloudflare/troubleshooting#failed-to-load-chunk
7. SSRF Vulnerability (CVE-2025-6087)
Vulnerability: Server-Side Request Forgery via /_next/image endpoint
Affected versions: @opennextjs/cloudflare < 1.3.0
Solution: Upgrade to version 1.3.0 or later:
npm install --save-dev @opennextjs/cloudflare@^1.3.0
Impact: Allows unauthenticated users to proxy arbitrary remote content
Source: https://github.com/advisories/GHSA-rvpw-p7vw-wj3m
8. Durable Objects Binding Warnings
Warning: "You have defined bindings to the following internal Durable Objects... will not work in local development, but they should work in production"
Cause: OpenNext uses Durable Objects for caching (DOQueueHandler, DOShardedTagCache)
Solution: Safe to ignore - warning is expected behavior
Alternative (to suppress warning): Define Durable Objects in separate Worker with own config
Source: https://opennext.js.org/cloudflare/known-issues#caching-durable-objects
9. Prisma + D1 Middleware Conflicts
Error: Build errors when using @prisma/client + @prisma/adapter-d1 in Next.js middleware
Cause: Database initialization in middleware context
Workaround: Initialize Prisma client in route handlers, not middleware
Source: https://github.com/opennextjs/opennextjs-cloudflare/issues/471
10. cross-fetch Library Errors
Error: Errors when using libraries that depend on cross-fetch
Cause: OpenNext patches deployment package causing cross-fetch to try using Node.js libraries when native fetch is available
Solution: Use native fetch API directly instead of cross-fetch:
// ✅ Use native fetch
const response = await fetch('https://api.example.com/data');
// ❌ Avoid cross-fetch
// import fetch from 'cross-fetch';
Source: https://opennext.js.org/cloudflare/troubleshooting
11. Windows Development Issues
Issue: Full Windows support not guaranteed
Cause: Underlying Next.js tooling issues on Windows
Solutions:
- Use WSL (Windows Subsystem for Linux)
- Use virtual machine with Linux
- Use Linux-based CI/CD for deployments
Source: https://opennext.js.org/cloudflare#windows-support
Feature Support Matrix
| Feature | Status | Notes |
|---|---|---|
| App Router | ✅ Fully Supported | Latest App Router features work |
| Pages Router | ✅ Fully Supported | Legacy Pages Router supported |
| Route Handlers | ✅ Fully Supported | API routes work as expected |
| React Server Components | ✅ Fully Supported | RSC fully functional |
| Server Actions | ✅ Fully Supported | Server Actions work |
| SSG | ✅ Fully Supported | Static Site Generation |
| SSR | ✅ Fully Supported | Server-Side Rendering |
| ISR | ✅ Fully Supported | Incremental Static Regeneration |
| Middleware | ✅ Supported | Except Node.js middleware (15.2+) |
| Image Optimization | ✅ Supported | Via Cloudflare Images |
| Partial Prerendering (PPR) | ✅ Supported | Experimental in Next.js |
| Composable Caching | ✅ Supported | 'use cache' directive |
| Response Streaming | ✅ Supported | Streaming responses work |
next/after API |
✅ Supported | Post-response async work |
| Node.js Middleware (15.2+) | ❌ Not Supported | Future support planned |
| Edge Runtime | ❌ Not Supported | Use Node.js runtime |
Integration with Cloudflare Services
D1 Database (SQL)
// app/api/users/route.ts
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const env = process.env as any;
const result = await env.DB.prepare(
'SELECT * FROM users WHERE active = ?'
).bind(true).all();
return Response.json(result.results);
}
export async function POST(request: NextRequest) {
const env = process.env as any;
const { name, email } = await request.json();
const result = await env.DB.prepare(
'INSERT INTO users (name, email) VALUES (?, ?)'
).bind(name, email).run();
return Response.json({ id: result.meta.last_row_id });
}
Wrangler config:
{
"d1_databases": [
{
"binding": "DB",
"database_name": "production-db",
"database_id": "your-database-id"
}
]
}
See also: cloudflare-d1 skill for complete D1 patterns
R2 Storage (Object Storage)
// app/api/upload/route.ts
import type { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const env = process.env as any;
const formData = await request.formData();
const file = formData.get('file') as File;
// Upload to R2
await env.BUCKET.put(file.name, file.stream(), {
httpMetadata: {
contentType: file.type
}
});
return Response.json({ success: true, filename: file.name });
}
export async function GET(request: NextRequest) {
const env = process.env as any;
const { searchParams } = new URL(request.url);
const filename = searchParams.get('file');
const object = await env.BUCKET.get(filename);
if (!object) {
return new Response('Not found', { status: 404 });
}
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream'
}
});
}
See also: cloudflare-r2 skill for complete R2 patterns
Workers AI (Model Inference)
// app/api/ai/route.ts
import type { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const env = process.env as any;
const { prompt } = await request.json();
const response = await env.AI.run('@cf/meta/llama-3-8b-instruct', {
prompt
});
return Response.json(response);
}
Wrangler config:
{
"ai": {
"binding": "AI"
}
}
See also: cloudflare-workers-ai skill for complete AI patterns
KV Storage (Key-Value)
// app/api/cache/route.ts
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const env = process.env as any;
const { searchParams } = new URL(request.url);
const key = searchParams.get('key');
const value = await env.KV.get(key);
return Response.json({ key, value });
}
export async function PUT(request: NextRequest) {
const env = process.env as any;
const { key, value, ttl } = await request.json();
await env.KV.put(key, value, { expirationTtl: ttl });
return Response.json({ success: true });
}
See also: cloudflare-kv skill for complete KV patterns
Image Optimization
Next.js image optimization works via Cloudflare Images. Configure in open-next.config.ts:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({
imageOptimization: {
loader: 'cloudflare'
}
});
Usage in components:
import Image from 'next/image';
export default function Avatar() {
return (
<Image
src="/avatar.jpg"
alt="User avatar"
width={200}
height={200}
// Automatically optimized via Cloudflare Images
/>
);
}
Billing: Cloudflare Images usage is billed separately
Docs: https://developers.cloudflare.com/images/
Caching Configuration
Configure caching behavior in open-next.config.ts:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({
// Custom cache configuration
cache: {
// Override default cache behavior
// See: https://opennext.js.org/cloudflare/caching
}
});
Default behavior: OpenNext provides sensible caching defaults
Advanced usage: See official OpenNext caching documentation
Known Limitations
Not Yet Supported
-
Node.js Middleware (Next.js 15.2+)
- Introduced in Next.js 15.2
- Support planned for future releases
- Use standard middleware for now
-
Edge Runtime
- Only Node.js runtime supported
- Remove
export const runtime = "edge"from your app
-
Full Windows Support
- Development on Windows not fully guaranteed
- Use WSL, VM, or Linux-based CI/CD
Worker Size Constraints
- Free plan: 3 MiB limit (gzip-compressed)
- Paid plan: 10 MiB limit (gzip-compressed)
- Monitor bundle size during development
- Use dynamic imports for code splitting
Database Connections
- External database clients (PostgreSQL, MySQL) must be request-scoped
- Cannot reuse connections across requests (Workers limitation)
- Prefer Cloudflare D1 for database needs (designed for Workers)
Deployment
Deploy from Local Machine
# Build and deploy in one command
npm run deploy
# Or step by step:
npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy
Deploy from CI/CD
Configure deployment command in your CI/CD system:
npm run deploy
Examples:
- GitHub Actions:
.github/workflows/deploy.yml - GitLab CI:
.gitlab-ci.yml - Cloudflare Workers Builds: Auto-detects
npm run deploy
Environment variables: Set secrets in Cloudflare dashboard or CI/CD system
Custom Domains
Add custom domain in Cloudflare dashboard:
- Navigate to Workers & Pages
- Select your Worker
- Settings → Domains & Routes
- Add custom domain
DNS: Domain must be on Cloudflare (zone required)
TypeScript Support
Generate types for Cloudflare bindings:
npm run cf-typegen
Creates cloudflare-env.d.ts with types for your bindings:
// cloudflare-env.d.ts (auto-generated)
interface CloudflareEnv {
DB: D1Database;
BUCKET: R2Bucket;
KV: KVNamespace;
AI: Ai;
}
Use in route handlers:
import type { NextRequest } from 'next/server';
export async function GET(request: NextRequest) {
const env = process.env as CloudflareEnv;
// Now env.DB, env.BUCKET, etc. are typed
}
Testing
Local Testing (Development)
# Next.js dev server (fast iteration)
npm run dev
Local Testing (Production-like)
# Workerd runtime (catches Workers-specific issues)
npm run preview
Integration Testing
Always test in preview mode before deploying:
# Build and run in workerd
npm run preview
# Test bindings (D1, R2, KV, AI)
# Test middleware
# Test API routes
# Test SSR/ISR behavior
Migration from Other Platforms
From Vercel
- Copy existing Next.js project
- Run existing project migration steps (above)
- Update environment variables in Cloudflare dashboard
- Replace Vercel-specific features:
- Vercel Postgres → Cloudflare D1
- Vercel Blob → Cloudflare R2
- Vercel KV → Cloudflare KV
- Vercel Edge Config → Cloudflare KV
- Test thoroughly with
npm run preview - Deploy with
npm run deploy
From AWS / Other Platforms
Same process as Vercel migration - the adapter handles Next.js standard features automatically.
Resources
Official Documentation
- OpenNext Cloudflare: https://opennext.js.org/cloudflare
- Cloudflare Next.js Guide: https://developers.cloudflare.com/workers/framework-guides/web-apps/nextjs/
- Next.js Docs: https://nextjs.org/docs
Troubleshooting
- Troubleshooting Guide: https://opennext.js.org/cloudflare/troubleshooting
- Known Issues: https://opennext.js.org/cloudflare/known-issues
- GitHub Issues: https://github.com/opennextjs/opennextjs-cloudflare/issues
Related Skills
cloudflare-worker-base- Base Worker setup with Hono + Vite + Reactcloudflare-d1- D1 database integrationcloudflare-r2- R2 object storagecloudflare-kv- KV key-value storagecloudflare-workers-ai- Workers AI integrationcloudflare-vectorize- Vector database for RAG
Quick Reference
Essential Commands
# New project
npm create cloudflare@latest -- my-next-app --framework=next
# Development
npm run dev # Fast iteration (Next.js dev server)
npm run preview # Test in workerd (production-like)
# Deployment
npm run deploy # Build and deploy to Cloudflare
# TypeScript
npm run cf-typegen # Generate binding types
Critical Configuration
// wrangler.jsonc
{
"compatibility_date": "2025-05-05", // Minimum!
"compatibility_flags": ["nodejs_compat"] // Required!
}
Common Pitfalls
- ❌ Using Edge runtime → ✅ Use Node.js runtime
- ❌ Global DB clients → ✅ Request-scoped clients
- ❌ Old compatibility_date → ✅ Use 2025-05-05+
- ❌ Missing nodejs_compat → ✅ Add to compatibility_flags
- ❌ Only testing in
dev→ ✅ Always testpreviewbefore deploy - ❌ Using Turbopack → ✅ Use standard Next.js build
Production Tested: Official Cloudflare support and active community Token Savings: ~59% vs manual setup Errors Prevented: 10+ documented issues Last Verified: 2025-10-21