Next.js
Cloudflare.Website.Nextjs deploys a Next.js
app as a Cloudflare Worker. It runs next build through the OpenNext
pipeline (@opennextjs/cloudflare), bundles the resulting server
into a self-contained Worker, and deploys client assets plus
prerendered pages as Worker static assets. There is no adapter to
configure and no Wrangler file.
App Router and Pages Router both work — server components, API
routes, middleware, server actions, dynamic segments, streaming SSR,
and getServerSideProps pages all run in the Worker.
Install
Section titled “Install”The build integration is not bundled with alchemy — the resource
dynamically imports @distilled.cloud/nextjs (and its peer,
OpenNext) from your project at deploy time, so both must be installed
alongside your framework. They are only used at build time, so dev
dependencies are enough:
bun add -d @distilled.cloud/nextjs @opennextjs/cloudflarenpm install -D @distilled.cloud/nextjs @opennextjs/cloudflarepnpm add -D @distilled.cloud/nextjs @opennextjs/cloudflareyarn add -D @distilled.cloud/nextjs @opennextjs/cloudflareConfigure OpenNext
Section titled “Configure OpenNext”Your next.config.* stays untouched. OpenNext needs one config file
next to it — the static-assets incremental cache is the
zero-infrastructure default, where prerendered ISR pages serve their
build-time payloads:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default defineCloudflareConfig({ incrementalCache: staticAssetsIncrementalCache,});For revalidation that actually writes, see Writable ISR below.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack) and derive the typed shape of its bindings from it:
import * as Cloudflare from "alchemy/Cloudflare";
export const Website = Cloudflare.Website.Nextjs("Website");
export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;WebsiteEnv is the typed shape of the Worker’s bindings, derived
from the class — you’ll import it into your Next.js code in
Read bindings in server code.
Add it to the Stack
Section titled “Add it to the Stack”Yield the class from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyNextjsSite", { providers: Cloudflare.providers(), state: Cloudflare.state(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);The OpenNext server runs under Node compatibility, so the
nodejs_compat compatibility flag is added automatically.
See examples/cloudflare-website-nextjs for the checked-in example.
Add bindings
Section titled “Add bindings”The resource returns a plain Worker, so env accepts the full
binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
secrets:
import * as Config from "effect/Config";
export const Uploads = Cloudflare.R2.Bucket("Uploads");
export const Website = Cloudflare.Website.Nextjs("Website", { env: { UPLOADS: Uploads, API_KEY: Config.redacted("API_KEY"), },});Uploads is a description, not a deploy — Alchemy provisions the
real bucket because the Website binds it. Config.redacted reads
API_KEY from your environment at deploy time and binds it as a
Worker secret — see Secrets & env.
Read bindings in server code
Section titled “Read bindings in server code”Route handlers, server components, and server actions read bindings
through OpenNext’s getCloudflareContext(). Its env is typed by
the global CloudflareEnv interface — extend it once with the
inferred env type:
import type { WebsiteEnv } from "./alchemy.run.ts";
declare global { interface CloudflareEnv extends WebsiteEnv {}}
export {};Every server entry point now gets fully typed bindings:
import { getCloudflareContext } from "@opennextjs/cloudflare";
export async function PUT(request: Request) { const { env } = getCloudflareContext(); await env.UPLOADS.put("hello.txt", await request.text()); return Response.json({ ok: true });}env.UPLOADS is typed as an R2 bucket and env.API_KEY as a
string — renaming a binding in alchemy.run.ts is a type error in
your routes.
Writable ISR
Section titled “Writable ISR”Switch the incremental cache to KV and revalidation actually writes:
revalidatePath / revalidateTag purge entries, and time-based
revalidate windows regenerate pages in the background through a
Durable Object queue hosted on the same Worker.
import { defineCloudflareConfig } from "@opennextjs/cloudflare";import kvIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/kv-incremental-cache";import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";import kvNextTagCache from "@opennextjs/cloudflare/overrides/tag-cache/kv-next-tag-cache";
export default defineCloudflareConfig({ incrementalCache: kvIncrementalCache, queue: doQueue, tagCache: kvNextTagCache,});Bind the pieces — the WORKER_SELF_REFERENCE self service binding
OpenNext uses to re-render pages is wired automatically:
export const IncCache = Cloudflare.KV.Namespace("NextIncCache");export const TagCache = Cloudflare.KV.Namespace("NextTagCache");
export const Website = Cloudflare.Website.Nextjs("Website", { env: { NEXT_INC_CACHE_KV: IncCache, NEXT_TAG_CACHE_KV: TagCache, NEXT_CACHE_DO_QUEUE: Cloudflare.DurableObject("NEXT_CACHE_DO_QUEUE", { className: "DOQueueHandler", }), },});DOQueueHandler ships inside the OpenNext worker bundle — the
binding declaration is all it takes.
Local dev
Section titled “Local dev”bun alchemy devalchemy dev defaults to preview parity: the OpenNext worker is
built and served under workerd locally, with the Worker’s bindings
emulated — the same runtime behavior, asset routing, and ISR/cache
semantics as production.
For an edit-refresh loop, switch to the real next dev (Turbopack
HMR):
export const Website = Cloudflare.Website.Nextjs("Website", { nextjs: { devMode: "hmr" },});In hmr mode your app code runs in Node with the Worker’s bindings
proxied onto getCloudflareContext() — no
initOpenNextCloudflareForDev() call needed. Cloudflare-specific
runtime behavior (workerd APIs, ISR semantics) still needs preview
mode.
Limitations
Section titled “Limitations”These come from upstream @opennextjs/cloudflare:
- Edge-runtime routes and pages (
export const runtime = "edge") are unsupported and would fail at runtime, so the build fails early with the offending route list. Remove the directive — the node runtime runs on Workers. Middleware is fully supported. next/imageoptimization requires a zone with Cloudflare Images. Onworkers.dev, passunoptimizedand images serve as raw static assets.- Partial Prerendering /
"use cache"and Pages-Routeri18nconfig are out of scope for now. App Router i18n via middleware works.