Deploy Next.js to Cloudflare Workers via the OpenNext adapter (@opennextjs/cloudflare). Use for SSR/ISR/SSG/App or Pages Router, getCloudflareContext, bindings (D1/R2/KV/AI/Hyperdrive), caching tiers, skew protection, multi-worker, custom worker, env vars, or worker size/runtime/keep_names/FinalizationRegistry/connection-scoping errors.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Deploy Next.js applications to Cloudflare Workers using the OpenNext adapter (@opennextjs/cloudflare). The adapter takes a standard Next.js build, runs package.json build script, then transforms the output to run on the Workers runtime using the Node.js compatibility layer (nodejs_compat) — not the Edge runtime.
| Requirement | Value | Why |
|---|---|---|
| Runtime | Node.js (default). Remove every export const runtime = "edge"; | Edge runtime is unsupported; OpenNext uses nodejs_compat. |
compatibility_flags | ["nodejs_compat", "global_fetch_strictly_public"] | Node APIs + allow fetch() in app code. |
compatibility_date | ≥ 2024-09-23; ≥ 2025-05-05 recommended (FinalizationRegistry) | Older dates break FinalizationRegistry, DOs, and more. |
| Wrangler | ≥ 3.99.0 to deploy; ≥ 4.13.0 for keep_names; ≥ 4.36.0 for stable remote bindings | Feature gates in the docs. |
| Next.js | v16 all minors/patches supported; latest minors of v14 and v15; v14 dropped Q1 2026 | Stated on the overview page. |
| Worker size (gzip) | 3 MiB Free / 10 MiB Paid (compressed only) | Hard Cloudflare limits. |
Windows: not fully guaranteed (Next.js tooling issues). Use WSL, a Linux VM, or Linux/macOS CI. See known issue #1305.
nextjsnextjs skill → framework/App Router/Server Components/Cache Components patterns, any platform (Vercel, self-hosted, ...). Use for async params, proxy.ts migration, "use cache".cloudflare-nextjs) → deploying Next.js to Workers via the OpenNext adapter: wrangler.jsonc, open-next.config.ts, getCloudflareContext, caching tiers, bindings, skew protection, multi-worker, the Workers-specific errors.proxy.ts caveat (Next 16): Next 16 renamed
middleware.ts→proxy.ts, but@opennextjs/cloudflaredoes not recognizeproxy.tsyet (issue #1277) — on Cloudflare, keep usingmiddleware.ts. This is the one place thenextjsskill's guidance does NOT apply here.
npm create cloudflare@latest -- my-next-app --framework=next --platform=workersC3 scaffolds a Next.js app, installs @opennextjs/cloudflare, creates wrangler.jsonc + open-next.config.ts + .dev.vars, wires package.json scripts, and (if R2 is enabled) creates an R2 bucket for caching.
npx @opennextjs/cloudflare migratemigrate automates: install adapter + wrangler, create wrangler.jsonc/open-next.config.ts/.dev.vars, update scripts, add public/_headers, add .open-next to .gitignore, wire initOpenNextCloudflareForDev() into next.config.ts, and create+configure an R2 cache bucket (only if R2 is enabled on the account).
npm install @opennextjs/cloudflare@latest
npm install --save-dev wrangler@latestThen create the three files (see references/wrangler.jsonc, references/open-next.config.ts, references/package.json) and add the dev/preview/deploy/upload/cf-typegen scripts. Pin adapter versions and audit before upgrading — see the dependency-upgrade skill.
// package.json
{
"dev": "next dev", // fast HMR via Next dev server
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview", // build + run in workerd locally
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy", // build + serve immediately
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload", // build + upload a version (gradual rollout)
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}dev — fastest feedback loop; add initOpenNextCloudflareForDev() to next.config.ts so getCloudflareContext() works locally with simulated/remote bindings.preview — runs in the actual Workers runtime (not Node). Always run before deploy to catch runtime-only issues.deploy — populates the remote cache, then wrangler deploy. App serves immediately.upload — populates remote cache, then wrangler versions upload. Does NOT serve automatically; for gradual deployments.build, preview, deploy, upload all implicitly call populateCache — you do not need to run it manually.
next.config.tsimport type { NextConfig } from "next";
const nextConfig: NextConfig = { /* ... */ };
export default nextConfig;
import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
initOpenNextCloudflareForDev();getCloudflareContext()Do NOT use process.env for bindings. The official API is getCloudflareContext() from @opennextjs/cloudflare.
import { getCloudflareContext } from "@opennextjs/cloudflare";
export async function GET() {
const { env, cf, ctx } = getCloudflareContext();
await env.MY_KV.put("foo", "bar");
return new Response(await env.MY_KV.get("foo"));
}Static routes (ISR/SSG) MUST use async mode — and be careful: secrets/local values are used during static generation.
const { env } = await getCloudflareContext({ async: true });TypeScript types: npm run cf-typegen generates cloudflare-env.d.ts (re-run after any binding change).
Remote bindings (local dev → real resources): stabilized in Wrangler 4.36.0. On older wrangler, enable via initOpenNextCloudflareForDev({ experimental: { remoteBindings: true } }) and use the experimental_remote (not remote) key on binding options. Note: remote bindings are also used during build.
Full patterns (D1/R2/KV/AI/Hyperdrive, Drizzle, Prisma, Stripe) → references/bindings-and-services.md.
OpenNext's cache has three parts: Incremental Cache (storage), Queue (dedupe/revalidate), Tag Cache (on-demand revalidateTag/revalidatePath).
| Site profile | Incremental | Queue | Tag Cache | When |
|---|---|---|---|---|
| SSG only (no revalidation) | staticAssetsIncrementalCache + enableCacheInterception: true | none | none | Fastest option; read-only |
| Small site (ISR/on-demand) | r2IncrementalCache | doQueue | d1NextTagCache | Low traffic; D1 tag cache |
| Large/high-traffic site | withRegionalCache(r2IncrementalCache, { mode: "long-lived" }) | doQueue | doShardedTagCache({ baseShardSize: 12 }) + purgeCache({ type: "direct" }) | DO-sharded; add cache purge if using on-demand |
Reserved binding names (do not reuse): ASSETS, WORKER_SELF_REFERENCE, NEXT_INC_CACHE_R2_BUCKET, NEXT_CACHE_DO_QUEUE, NEXT_TAG_CACHE_D1, NEXT_TAG_CACHE_DO_SHARDED, NEXT_CACHE_DO_PURGE, IMAGES.
CACHE_PURGE_API_TOKEN + CACHE_PURGE_ZONE_ID secrets).res.revalidate requires a self-reference service binding named WORKER_SELF_REFERENCE.next.config.ts headers() for public/ and immutable build files do not apply. Use public/_headers.Deep dive (all options, env vars, regional modes, migration from 0.6) → references/caching.md and references/known-issues.md.
cache() from react; maxUses: 1 on PG pools; getCloudflareContext({ async: true }) for ISR/SSG; Prisma needs previewFeatures = ["driverAdapters"], no output dir in schema.prisma, and serverExternalPackages: ["@prisma/client", ".prisma/client"]. → references/bindings-and-services.mdnode:https; pass httpClient: Stripe.createFetchHttpClient(). → references/bindings-and-services.mdimages.binding: "IMAGES" in wrangler.jsonc, or a custom loader (/cdn-cgi/image/...) for zones. minimumCacheTTL and dangerouslyAllowLocalIP are not supported; custom loader bypasses middleware and ignores remotePatterns. → references/advanced.md.env files (not just .dev.vars); NEXTJS_ENV in .dev.vars selects the env; --keep-vars on deploy; secrets are write-only. → references/dev-deploy-and-env.mdscheduled, Durable Object exports) — point main at your worker that re-exports the generated fetch handler. → references/advanced.md@opennextjs/cloudflare deploy. → references/advanced.mdcloudflare.skewProtection.enabled, run_worker_first: true, getDeploymentId(), env vars CF_WORKER_NAME/CF_PREVIEW_DOMAIN/CF_WORKERS_SCRIPTS_API_TOKEN/CF_ACCOUNT_ID. Disabled for Workers with a Durable Object (move DOs to a separate worker). → references/advanced.mdreferences/error-catalog-extended.md)"Your Worker exceeded the size limit of 3 MiB" (Free) / "10 MiB" (Paid). Only gzip size counts. Free → upgrade to Paid. Paid → analyze bundle: npx @opennextjs/cloudflare build, then inspect .open-next/server-functions/default/handler.mjs.meta.json (visualize with ESBuild Bundle Analyzer); remove unused deps, use dynamic imports.
Global DB client (e.g. postgres, pg Pool) reused across requests. Create the client inside the request handler (or use cache() from react), and maxUses: 1 for PG pools.
Enable nodejs_compat, ensure compatibility_date ≥ 2024-09-23. Some packages ship a workerd export — add them to serverExternalPackages in next.config.ts (e.g. @prisma/client, .prisma/client, postgres, jose, react-textarea-autosize, @libsql/isomorphic-ws). Or set .env: WRANGLER_BUILD_CONDITIONS="" + WRANGLER_BUILD_PLATFORM="node".
/_next/image SSRF. Upgrade immediately: @opennextjs/cloudflare@^1.3.0 (current: ^1.18.1).
server/chunks/ssr/<name>.jsOutdated adapter with Turbopack builds. Upgrade @opennextjs/cloudflare to latest, or switch to webpack (next build without --turbo).
ReferenceError: FinalizationRegistry is not definedcompatibility_date too old. Set "compatibility_date": "2025-05-05" (or later) in wrangler.jsonc.
Uncaught ReferenceError: __name is not definedWrangler's esbuild keep-names injects __name into generated script strings that some libs (e.g. next-themes) eval at runtime. Set "keep_names": false in wrangler.jsonc (requires Wrangler ≥ 4.13.0). You lose original function names in debugging.
populateCache remoteAccount protected by Cloudflare Access blocks the open-next-cache-populate helper worker. Do not create a separate Access app for it; add a Service Auth policy (Include = Any Access Service Token) to the existing app covering *.<account>.workers.dev, create a service token, and export CLOUDFLARE_ACCESS_CLIENT_ID / CLOUDFLARE_ACCESS_CLIENT_SECRET.
Always check the issue tracker — these are recurring at the time of writing:
| # | Bug | Workaround |
|---|---|---|
| #1171 | v1.18.0 breaks R2 cache population (pinned) | Pin to 1.17.x or upgrade past the fix |
| #1277 | proxy.js not supported — Next 16 proxy.ts rename breaks routing | Keep middleware.ts on Cloudflare |
| #1130 / #1225 | cacheComponents: true crashes (Unexpected identifier '$' / Connection closed) | Disable cacheComponents |
| #1321 | Intermittent React hydration mismatch (~9% of loads) | — |
| #1322 / #1214 | Hyperdrive + pg / @prisma/adapter-pg bundling failure | — |
| #1315 | Time-based fetch-cache revalidation silently no-ops on Next 16 (deployed) | — |
| #1305 | Windows + Turbopack routes 500 | Use Linux/macOS or webpack |
| #1317 | @cf-wasm/photon Turbopack build fails (raw .wasm) | Use webpack |
| #1326 | Webpack chunk inlining misses named chunks → Unknown chunk N | — |
| #617 | Node middleware (Next 15.2+) unsupported (feature request) | Use standard middleware |
Full tracker: https://github.com/opennextjs/opennextjs-cloudflare/issues
| Feature | Status | Notes |
|---|---|---|
| App Router, Pages Router, Route Handlers, Dynamic routes | ✅ | Full |
| React Server Components, Server Actions | ✅ | Full |
| SSG, SSR, ISR | ✅ | Full |
| Middleware | ✅ | Except Node middleware (Next 15.2+, issue #617) |
| Image optimization | ✅ | Via Cloudflare Images (binding or custom loader) |
| Partial Prerendering (PPR) | ✅ | But cache interception + PPR incompatible today |
Composable Caching ('use cache'), after | ✅ | |
| Turbopack | ✅ | But see #1305, #1317, #1326 — webpack is safer |
| Edge Runtime | ❌ | Node runtime only; remove runtime = "edge" |
| Node Middleware (15.2+) | ❌ | #617 |
| Skill | Use for |
|---|---|
nextjs | Next.js framework/App Router patterns on any platform (the proxy.ts/cache/Server Components reference) |
cloudflare-workers | Generic Workers patterns; framework decision tree (Hono vs OpenNext) |
drizzle-orm-d1 | Drizzle + D1 deep dive (note: OpenNext must not bundle Wrangler — see its error catalog) |
cloudflare-r2 / cloudflare-kv / cloudflare-d1 | Service-specific deep dives |
dependency-upgrade | Pinning/auditing @opennextjs/cloudflare (production traffic) |
| File | Load when |
|---|---|
references/caching.md | Choosing/configuring incremental/queue/tag cache, regional cache, cache purge |
references/bindings-and-services.md | Integrating D1/R2/KV/AI/Hyperdrive, Drizzle/Prisma request-scoped clients, Stripe |
references/dev-deploy-and-env.md | Setting up dev/preview/deploy, Workers Builds CI, env vars/secrets |
references/advanced.md | Custom worker, multi-worker, skew protection, static assets, keep_names, workerd packages, image optimization |
references/known-issues.md | DO build warnings, migrating 0.6 → 1.0.0-beta |
references/error-catalog-extended.md | Any error beyond the top 8 above |
references/troubleshooting.md | Step-by-step debugging + profiling/minification |
references/feature-support.md | Detailed feature compatibility matrix |
references/wrangler.jsonc | Small-site and large-site wrangler templates (all reserved bindings) |
references/open-next.config.ts | The three caching tiers as runnable configs |
references/database-client-example.ts | Request-scoped DB client patterns |
references/package.json | Reference scripts + versions |
Version: @opennextjs/cloudflare ^1.18.1 · Next.js 14/15/16 · Wrangler ≥ 3.99.0 · compatibility_date ≥ 2025-05-05
Last Verified: 2026-08-05
8837836
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.