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. No OpenNext config or
Wrangler file is required.
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. Install
@alchemy.run/frontend-frameworks and its OpenNext peer; the resource
loads the package’s /nextjs export from your project at deploy time.
They are only used at build time, so dev dependencies are enough:
bun add -d @alchemy.run/frontend-frameworks @opennextjs/cloudflarenpm install -D @alchemy.run/frontend-frameworks @opennextjs/cloudflarepnpm add -D @alchemy.run/frontend-frameworks @opennextjs/cloudflareyarn add -D @alchemy.run/frontend-frameworks @opennextjs/cloudflareAlchemy leaves native next.config.* and Tailwind configuration unchanged.
No Alchemy plugin is required. OpenNext configuration is optional; without it,
the default cache serves prerendered pages from static assets. Use
Writable ISR for revalidation.
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.
Optional native OpenNext configuration
Section titled “Optional native OpenNext configuration”The resource declaration is the same in both cases:
export const Website = Cloudflare.Website.Nextjs("Website");- Without
open-next.config.ts: Alchemy generates a temporary config with read-only static-assets caching, or matching KV adapters whenisris set. You do not need to create a config file. - With
open-next.config.ts: Alchemy automatically loads the file fromrootDir(the working directory by default) through OpenNext’s native compiler. It never rewrites the file. Imports and callbacks are preserved, and you can use the full nativeOpenNextConfigshape within Cloudflare’s adapter constraints. This does not make AWS-specific runtime features available on Workers.
For example, an optional native config retaining read-only static-assets caching:
import { defineCloudflareConfig, type OpenNextConfig } from "@opennextjs/cloudflare";import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
export default { ...defineCloudflareConfig({ incrementalCache: staticAssetsIncrementalCache }), buildCommand: "pnpm exec next build",} satisfies OpenNextConfig;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.
export const IncCache = Cloudflare.KV.Namespace("NextIncCache");export const TagCache = Cloudflare.KV.Namespace("NextTagCache");
export const Website = Cloudflare.Website.Nextjs("Website", { isr: { incrementalCache: IncCache, tagCache: TagCache },});Alchemy binds the cache namespaces, revalidation queue, and Worker
self-reference. Without a native config, it also selects the matching adapters.
If you supply open-next.config.ts, select kv-incremental-cache,
kv-next-tag-cache, and do-queue there to match these bindings: isr does
not override the file’s adapter choices. The static-assets example above is
read-only, not a writable-ISR configuration.
The KV cache fills on demand; build-time cache entries are not uploaded to KV. Pages using writable ISR must be able to regenerate with runtime credentials and data sources.
Build options
Section titled “Build options”export const Website = Cloudflare.Website.Nextjs("Website", { openNext: { buildCommand: "pnpm exec next build", minify: true },});An explicit openNext.buildCommand overrides the native config’s
buildCommand. If neither sets a command, the default is npx next build.
openNext.minify and openNext.debug remain resource-level build controls.
Rebuild scope
Section titled “Rebuild scope”Unchanged inputs skip the OpenNext build. By default, project files outside
build outputs and node_modules, plus the nearest package-manager lockfile,
participate in the content hash.
The native open-next.config.ts itself is always hashed, even when memo
narrows the scope. When narrowing memo.include, include any helpers imported
by the config too, for example config/**:
export const Website = Cloudflare.Website.Nextjs("Website", { memo: { include: ["app/**", "public/**", "package.json", "next.config.mjs", "config/**"], },});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", { dev: { mode: "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.