1---
2title: Next.js
3description: Deploy a Next.js app to Cloudflare Workers with Cloudflare.Website.Nextjs — the OpenNext pipeline, writable ISR, and wrangler-free local dev.
4sidebar:
5 order: 9.5
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks @opennextjs/cloudflare";
11
12`Cloudflare.Website.Nextjs` deploys a [Next.js](https://nextjs.org)
13app as a Cloudflare Worker. It runs `next build` through the OpenNext
14pipeline (`@opennextjs/cloudflare`), bundles the resulting server
15into a self-contained Worker, and deploys client assets plus
16prerendered pages as Worker static assets. No OpenNext config or
17Wrangler file is required.
18
19App Router and Pages Router both work — server components, API
20routes, middleware, server actions, dynamic segments, streaming SSR,
21and `getServerSideProps` pages all run in the Worker.
22
23## Install
24
25The build integration is not bundled with alchemy. Install
26`@alchemy.run/frontend-frameworks` and its OpenNext peer; the resource
27loads the package's `/nextjs` export from your project at deploy time.
28They are only used at build time, so dev dependencies are enough:
29
30<Tabs syncKey="pkgManager">
31 <TabItem label="bun" icon="bun">
32 <Code code={`bun add -d ${devDeps}`} lang="sh" />
33 </TabItem>
34 <TabItem label="npm" icon="npm">
35 <Code code={`npm install -D ${devDeps}`} lang="sh" />
36 </TabItem>
37 <TabItem label="pnpm" icon="pnpm">
38 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
39 </TabItem>
40 <TabItem label="yarn" icon="seti:yarn">
41 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
42 </TabItem>
43</Tabs>
44
45Alchemy leaves native `next.config.*` and Tailwind configuration unchanged.
46No Alchemy plugin is required. OpenNext configuration is optional; without it,
47the default cache serves prerendered pages from static assets. Use
48[Writable ISR](#writable-isr) for revalidation.
49
50## Declare the Website
51
52Declare the site as a module-level const (rather than inline in the
53Stack) and derive the typed shape of its bindings from it:
54
55```typescript
56// alchemy.run.ts
57import * as Cloudflare from "alchemy/Cloudflare";
58
59export const Website = Cloudflare.Website.Nextjs("Website");
60
61export type WebsiteEnv = Cloudflare.InferEnv<typeof Website>;
62```
63
64`WebsiteEnv` is the typed shape of the Worker's bindings, derived
65from the class — you'll import it into your Next.js code in
66[Read bindings in server code](#read-bindings-in-server-code).
67
68## Optional native OpenNext configuration
69
70The resource declaration is the same in both cases:
71
72```typescript
73export const Website = Cloudflare.Website.Nextjs("Website");
74```
75
76- **Without `open-next.config.ts`:** Alchemy generates a temporary config with
77 read-only static-assets caching, or matching KV adapters when `isr` is set.
78 You do not need to create a config file.
79- **With `open-next.config.ts`:** Alchemy automatically loads the file from
80 `rootDir` (the working directory by default) through OpenNext's native compiler.
81 It never rewrites the file. Imports and callbacks are preserved, and you can
82 use the full native `OpenNextConfig` shape within Cloudflare's adapter
83 constraints. This does not make AWS-specific runtime features available on Workers.
84
85For example, an optional native config retaining read-only static-assets caching:
86
87```typescript
88// open-next.config.ts
89import { defineCloudflareConfig, type OpenNextConfig } from "@opennextjs/cloudflare";
90import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
91
92export default {
93 ...defineCloudflareConfig({ incrementalCache: staticAssetsIncrementalCache }),
94 buildCommand: "pnpm exec next build",
95} satisfies OpenNextConfig;
96```
97
98## Add it to the Stack
99
100Yield the class from your Stack and return its URL:
101
102```typescript
103// alchemy.run.ts
104import * as Alchemy from "alchemy";
105import * as Effect from "effect/Effect";
106
107export default Alchemy.Stack(
108 "MyNextjsSite",
109 {
110 providers: Cloudflare.providers(),
111 state: Cloudflare.state(),
112 },
113 Effect.gen(function* () {
114 const site = yield* Website;
115 return { url: site.url };
116 }),
117);
118```
119
120The OpenNext server runs under Node compatibility, so the
121`nodejs_compat` compatibility flag is added automatically.
122
123See
124[examples/cloudflare-website-nextjs](https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-website-nextjs)
125for the checked-in example.
126
127## Add bindings
128
129The resource returns a plain `Worker`, so `env` accepts the full
130binding vocabulary — KV namespaces, R2 buckets, Durable Objects,
131secrets:
132
133```diff lang="typescript"
134// alchemy.run.ts
135+import * as Config from "effect/Config";
136
137+export const Uploads = Cloudflare.R2.Bucket("Uploads");
138
139export const Website = Cloudflare.Website.Nextjs("Website", {
140+ env: {
141+ UPLOADS: Uploads,
142+ API_KEY: Config.Redacted("API_KEY"),
143+ },
144});
145```
146
147`Uploads` is a description, not a deploy — Alchemy provisions the
148real bucket because the Website binds it. `Config.Redacted` reads
149`API_KEY` from your environment at deploy time and binds it as a
150Worker secret — see [Secrets & env](/cloudflare/security/secrets-env).
151
152## Read bindings in server code
153
154Route handlers, server components, and server actions read bindings
155through OpenNext's `getCloudflareContext()`. Its `env` is typed by
156the global `CloudflareEnv` interface — extend it once with the
157inferred env type:
158
159```typescript
160// cloudflare-env.d.ts
161import type { WebsiteEnv } from "./alchemy.run.ts";
162
163declare global {
164 interface CloudflareEnv extends WebsiteEnv {}
165}
166
167export {};
168```
169
170Every server entry point now gets fully typed bindings:
171
172```typescript
173// app/api/upload/route.ts
174import { getCloudflareContext } from "@opennextjs/cloudflare";
175
176export async function PUT(request: Request) {
177 const { env } = getCloudflareContext();
178 await env.UPLOADS.put("hello.txt", await request.text());
179 return Response.json({ ok: true });
180}
181```
182
183`env.UPLOADS` is typed as an R2 bucket and `env.API_KEY` as a
184string — renaming a binding in `alchemy.run.ts` is a type error in
185your routes.
186
187## Writable ISR
188
189Switch the incremental cache to KV and revalidation actually writes:
190`revalidatePath` / `revalidateTag` purge entries, and time-based
191`revalidate` windows regenerate pages in the background through a
192Durable Object queue hosted on the same Worker.
193
194
195```typescript
196// alchemy.run.ts
197export const IncCache = Cloudflare.KV.Namespace("NextIncCache");
198export const TagCache = Cloudflare.KV.Namespace("NextTagCache");
199
200export const Website = Cloudflare.Website.Nextjs("Website", {
201 isr: { incrementalCache: IncCache, tagCache: TagCache },
202});
203```
204
205Alchemy binds the cache namespaces, revalidation queue, and Worker
206self-reference. Without a native config, it also selects the matching adapters.
207If you supply `open-next.config.ts`, select `kv-incremental-cache`,
208`kv-next-tag-cache`, and `do-queue` there to match these bindings: `isr` does
209not override the file's adapter choices. The static-assets example above is
210read-only, not a writable-ISR configuration.
211
212The KV cache fills on demand; build-time cache entries are not uploaded to KV.
213Pages using writable ISR must be able to regenerate with runtime credentials
214and data sources.
215
216## Build options
217
218```typescript
219export const Website = Cloudflare.Website.Nextjs("Website", {
220 openNext: { buildCommand: "pnpm exec next build", minify: true },
221});
222```
223
224An explicit `openNext.buildCommand` overrides the native config's
225`buildCommand`. If neither sets a command, the default is `npx next build`.
226`openNext.minify` and `openNext.debug` remain resource-level build controls.
227
228## Rebuild scope
229
230Unchanged inputs skip the OpenNext build. By default, project files outside
231build outputs and `node_modules`, plus the nearest package-manager lockfile,
232participate in the content hash.
233
234The native `open-next.config.ts` itself is always hashed, even when `memo`
235narrows the scope. When narrowing `memo.include`, include any helpers imported
236by the config too, for example `config/**`:
237
238```typescript
239export const Website = Cloudflare.Website.Nextjs("Website", {
240 memo: {
241 include: ["app/**", "public/**", "package.json", "next.config.mjs", "config/**"],
242 },
243});
244```
245
246## Local dev
247
248```sh
249bun alchemy dev
250```
251
252`alchemy dev` defaults to **preview parity**: the OpenNext worker is
253built and served under workerd locally, with the Worker's bindings
254emulated — the same runtime behavior, asset routing, and ISR/cache
255semantics as production.
256
257For an edit-refresh loop, switch to the real `next dev` (Turbopack
258HMR):
259
260```typescript
261export const Website = Cloudflare.Website.Nextjs("Website", {
262 dev: { mode: "hmr" },
263});
264```
265
266In hmr mode your app code runs in Node with the Worker's bindings
267proxied onto `getCloudflareContext()` — no
268`initOpenNextCloudflareForDev()` call needed. Cloudflare-specific
269runtime behavior (workerd APIs, ISR semantics) still needs preview
270mode.
271
272## Limitations
273
274These come from upstream `@opennextjs/cloudflare`:
275
276- **Edge-runtime routes and pages** (`export const runtime = "edge"`)
277 are unsupported and would fail at runtime, so the build fails early
278 with the offending route list. Remove the directive — the node
279 runtime runs on Workers. Middleware is fully supported.
280- **`next/image` optimization** requires a zone with Cloudflare
281 Images. On `workers.dev`, pass `unoptimized` and images serve as
282 raw static assets.
283- **Partial Prerendering / `"use cache"`** and Pages-Router `i18n`
284 config are out of scope for now. App Router i18n via middleware
285 works.