NextJS on Cloudflare Wiki

Published on

Initialize, write and deploy NextJS applications optimized for running on Cloudflare.

Platforma
  • Next.js is an open-source web development framework created by the private company Vercel providing React-based web applications with server-side rendering and static rendering, see more at Next.JS page.
  • Cloudflare is one of the biggest network service for the purposes of increasing the security and performance of web sites and services. It also provides a cloud service to host files and applications.

3 Options

Static NextJs build on Cloudflare worker

This option consists of uploading static files generated bz next to cloudflare worker. As it|s a static deliverz, without any operations on background, this is the fastest delivery you can reach. Cloudflare worker is associated with a limit of maximally 100 000 files per worker. If zou need more files (you have a big website), there is a need to use either more workers with properly set routing or Cloudflare pages. Another option is switch to standard OpenNext worker or a combination of static + Open Next worker.

  1. Initialize a NextJs app and set it for Cloudflare:
    npx create-next-app@latest <appName>

    You can follow options from https://vrealmatic.com/typescript/nextjs.

  2. Add output: "export", trailingSlash: true, to next.config.ts. The file may look as follow:
    import type { NextConfig } from "next";
    
    const nextConfig: NextConfig = {
      output: "export",
      trailingSlash: true,
    };
    
    export default nextConfig;
  3. Add wrangler.jsonc file to root
    {
      "$schema": "./node_modules/wrangler/config-schema.json",
      "name": "next444",
      "main": "src/worker.ts",
      "compatibility_date": "2026-03-04",
      "assets": {
        "directory": "./out",
        "binding": "ASSETS",
        "run_worker_first": ["/api/*"]
      }
    }
  4. Add small worker src/worker.ts
    interface AssetsBinding {
      fetch(input: Request | URL | string, init?: RequestInit): Promise<Response>;
    }
    
    interface Env {
      ASSETS: AssetsBinding;
    }
    
    export default {
      async fetch(request: Request, env: Env): Promise<Response> {
        const url = new URL(request.url);
    
        if (url.pathname === "/api/hello") {
          return Response.json({
            ok: true,
            message: "Hello from Worker API",
          });
        }
    
        return env.ASSETS.fetch(request);
      },
    };
  5. Install Wrangler
    npm install -D wrangler

Develop, build, deploy

According to package.json file, you can manage nextjs app / server actions, such as:

  • Preview:

    npx wrangler dev
    , alternatively npm run dev
  • Build static output

    npm run build

    Generates Out directory with static files for statc use

  • Deploy:

    npx wrangler deploy

We used this option for games44.com website.

OpenNext worker

NextJs server-based worker on Cloudflare.

Initialize a NextJs Worker app for Cloudflare:

npm create cloudflare@latest -- my-next-app --framework=next --platform=workers
  • You're in an existing git repository. Do you want to use git for version control? - choose Yes
  • Do you want to deploy your application? choose Yes or No based on your preference.

Run the app on localhost options:

  1. npm run dev
  2. npx wrangler dev

Deploy / Redeploy (from Ubuntu WSL on Windows)

NOTE: If you need to deploy applications to more Cloudflare accounts, use Deploying with a token.

  1. Be sure you are authentizated / use proper token
    npx wrangler whoami
  2. Build the app for Cloudflare Worker
    npx @opennextjs/cloudflare build
  3. npx wrangler deploy

Check real-time logs on deployed ver through wrangler

wrangler tail

Upgrading to latest version

  • Check curent situation
    node -v npm ls next react react-dom @opennextjs/cloudflare wrangler npm outdated
  • Create a Git for for the upgrade to be able return back if anything goes wrong
    git checkout -b upgrade-next
  • Upgrade NextJs and others
    npx @next/codemod@canary upgrade latest npm install @opennextjs/cloudflare@latest npm install -D wrangler@latest npm install -D @types/react@latest @types/react-dom@latest
  • Check situatuion after upgrade
    npm ls next react react-dom @opennextjs/cloudflare wrangler

    If you got overrides error (upgrade sometimes adds useless overrides (you can check it compared to git prior upgrade)), verify and remove them.

    npm pkg get overrides npm pkg get devDependencies.@types/react npm pkg get devDependencies.@types/react-dom
    npm pkg delete overrides.@types/react npm pkg delete overrides.@types/react-dom

    After removing overrides, process clean reinstalation

    rm -rf node_modules package-lock.json npm install npm ls next react react-dom @opennextjs/cloudflare wrangler
  • Test the project
    • npm run dev
    • npx wrangler dev
    • npm run build
    • npm run preview
  • If everything ok, you can merge the git upgrade branch to your main repository
    • Check differences
      git status git diff

      diff view can be exited with q key

    • Commit upgrade to the new branch
      git add . git commit -m "Upgrade Next.js and update OpenNext Cloudflare"
    • Merge upgrade to main repository
      git checkout main git merge upgrade-next16
    • Push the upgraded project to git
      git push -u origin main

Combination of Static Build + Open Worker on background

You can combine both approaches together. Have 2 workers, 1 static and second OpenWorker. Configurate static so, that if there is no static file found, then it's processed with the OpenNext worker.

For simplicity, you can keep both the workers within one file and manage it all through package.json, see:

{
	"name": "vrealmatic",
	"version": "0.1.0",
	"private": true,
	"scripts": {
		"dev": "next dev --turbopack",
		"prebuild:static": "if [ -d src/app/api ]; then mv src/app/api src/app/_api_runtime_only; fi",
		"build:static": "NEXT_BUILD_TARGET=static next build",
		"postbuild:static": "if [ -d src/app/_api_runtime_only ]; then mv src/app/_api_runtime_only src/app/api; fi",
		"dev:static": "npm run build:static && wrangler dev --config wrangler.static.jsonc",
		"deploy:static": "npm run build:static && wrangler deploy --config wrangler.static.jsonc",
    	"build": "next build",
		"start": "next start",
		"lint": "next lint",
		"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
		"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
		"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
		"cf-typegen": "wrangler types --env-interface CloudflareEnv ./cloudflare-env.d.ts"
	},
	"dependencies": {
		"@opennextjs/cloudflare": "^1.14.4",
		"next": "15.5.9",
		"react": "19.1.4",
		"react-dom": "19.1.4"
	},
	"devDependencies": {
		"@cloudflare/workers-types": "^4.20260103.0",
		"@eslint/eslintrc": "^3",
		"@tailwindcss/postcss": "^4",
		"@types/node": "^20.19.27",
		"@types/react": "^19",
		"@types/react-dom": "^19",
		"eslint": "^9",
		"eslint-config-next": "15.4.6",
		"tailwindcss": "^4",
		"typescript": "^5",
		"wrangler": "^4.54.0"
	}
}

Creating D1 database and connecting it

Either throgh Cloudflare UI, or through wrangler in terminal - In project root folder terminal, run

npx wrangler d1 create my-next-app-db

D1 Typing

npx wrangler types --env-interface CloudflareEnv

Connect DB

  • In wrangler.jsonc, add
    "d1_databases": [
            {
                "binding": "my_db",
                "database_name": "my-next-app-db",
                "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                "remote": true
            }
        ]
  • Then in any script you need the db, or in a lib
    import { getCloudflareContext } from "@opennextjs/cloudflare";
    
    type Env = { my_db: D1Database; // binding name in wrangler.jsonc };
    
    export function getD1() {
      const { env } = getCloudflareContext() as unknown as { env: Env };
      const db = env.my_db;
    }

Integration R2 Storage Database to NextJS application

R2 Storage Database can be configured in CloudFlare UI: Storage & Databases / R2 Object Storage. Alternatively, you can create R2 storage database also through cli, see Create new buckets.

R2 Storage can be used for holding any type of files, from common web media up to cache jsons.

Config wrangler.jsonc

"r2_buckets": [
    {
        "binding": "NEXT_INC_CACHE_R2_BUCKET",
        "bucket_name": "cache"
    }
],

Sample R2 Library src/lib/r2.ts

import { getCloudflareContext } from "@opennextjs/cloudflare";

type Env = { NEXT_INC_CACHE_R2_BUCKET: R2Bucket; };
   
const r2CacheKey : string = 'vrealmatic_page/v3/';
       
async function getBucket(): Promise<R2Bucket> {
    const ctx = getCloudflareContext() as { env: Env };
    const bucket = ctx.env.NEXT_INC_CACHE_R2_BUCKET;
    if (!bucket) throw new Error("R2 binding NEXT_INC_CACHE_R2_BUCKET is missing in Cloudflare context env.");
    return bucket;
}
            
export async function r2GetJson<T>(key: string): Promise<T | null> {
    const bucket = await getBucket();
    const obj = await bucket.get(r2CacheKey+key);
    if (!obj) return null;
    return await obj.json<T>();
}
            
export async function r2PutJson(key: string, value: unknown): Promise<void> {
    const bucket = await getBucket();
    await bucket.put((r2CacheKey+key), JSON.stringify(value), { httpMetadata: { contentType: "application/json" } });
}

export function r2_runInBackground(promise: Promise<unknown>) {
  try {
    const ctx = getCloudflareContext()?.ctx as
      | { waitUntil?: (p: Promise<unknown>) => void }
      | undefined;

    if (ctx?.waitUntil) {
      ctx.waitUntil(promise);
      return;
    }
  } catch {
    // next dev / mimo CF context
  }

  void promise.catch((err) => {
    console.error("Background task failed", err);
  });
}

Use Sample for saving cache data to R2

type R2Cached<T> = {
    ts: number;     // stored at (ms)
    data: T;
};
                    
const PAGE_CTX_TTL_MS = 86_400_000; // 24h
                    
const inFlightPageContext = new Map<string, Promise<PageContext>>();
async function GetPageContext_TTL(normalizedPath: string) : Promise<PageContext> {
	const key = `${normalizedPath}/context`;

	const existing = inFlightPageContext.get(key);
   	if (existing) return existing;

	let staleCached: PageContext | null = null;

	const task = (async (): Promise<PageContext> => {
		const now = Date.now();
		// 1) try cache
		const cached = await r2GetJson<R2Cached<PageContext>>(key);
		if (cached) {
			if ((now - cached.ts) < PAGE_CTX_TTL_MS) return cached.data;
			staleCached = cached.data;
		}
		// 2) MISS/STALE => DB
		const data: PageContext = await GetPageContext(normalizedPath);
		if (data.page) {
			// 3) write back asynchronously
			r2_runInBackground(
				r2PutJson(key, { ts: now, data } satisfies R2Cached<PageContext>).catch((err) => { console.error("r2PutJson failed", { key, err });})
			);
		}
		return data;
	})();

	const shared = withTimeout(
		task,
		6000,
		`GetPageContext_TTL(${normalizedPath})`
	).catch((err) => {
		console.error("GetPageContext_TTL failed", { key, normalizedPath, err });
		if (staleCached) return staleCached;
		throw err;
	});

	inFlightPageContext.set(key, shared);

	void shared.finally(() => {
		if (inFlightPageContext.get(key) === shared) inFlightPageContext.delete(key);
	}).catch(() => {});
  	return shared;
};

export function getPageContextCache(slug?: string[]) {
  const baseUrlPath = slugToPath(slug ?? []);
  return getPageContextCacheProcess(baseUrlPath);
}
export const getPageContextCacheProcess = cache(GetPageContext_TTL);

Incremental cache for NextJS on Cloudflare

  1. At wrangler.jsonc, add bindings to R2 bucket and Durable Object + migration, such as:
    "r2_buckets": [
    		{
    			"binding": "NEXT_INC_CACHE_R2_BUCKET",
    			"bucket_name": "inc-cache"
    		},
    		//...
    	],
    "durable_objects": {
    		"bindings": [
    			{
    				"name": "NEXT_CACHE_DO_QUEUE",
    				"class_name": "DOQueueHandler"
    			}
    		]
    	},
    "migrations": [
    		{ "tag": "v1", "new_sqlite_classes": ["DOQueueHandler"] },
    	],
  2. At open-next.config.ts, attach incremenatl cache configuration, such as:
    import { defineCloudflareConfig } from "@opennextjs/cloudflare";
    import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
    import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
    import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
    
    export default defineCloudflareConfig({
    	// See https://opennext.js.org/cloudflare/caching for more details
    	incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }), 
    	queue: doQueue,
    });
  3. Expand your worker-entry.mjs for DO
    import openNextWorker from "./.open-next/worker.js";
    export { DOQueueHandler } from "./.open-next/worker.js";
    
    export default {
      async fetch(request, env, ctx) {
        const url = new URL(request.url);
    
        if (url.pathname === "/en" || url.pathname === "/en/") {
          url.pathname = "/";
          return Response.redirect(url.href, 308);
        }
    
        if (url.hostname === "xxx.com") {
          url.hostname = "www.xxx.com";
          url.protocol = "https:";
          return Response.redirect(url.href, 308);
        }
    
        if (url.hostname.endsWith(".workers.dev")) {
          url.hostname = "www.xxx.com";
          url.protocol = "https:";
          return Response.redirect(url.href, 308);
        }
    
        return withEdgeCache(request, ctx, (req) => openNextWorker.fetch(req, env, ctx));
      },
    };
    
  4. Build & Deploy

This mechanism uses default doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue". DO is created automatically. There is nothing next to do above the steps described above.

This mechanism eliminates observability errors of type "Failed to revalidate stale page ... FatalError: Dummy queue is not implemented".

Vrealmatic consulting

Anything unclear?

Let us know!

Contact Us