1---
2title: Next.js
3description: Deploy a Next.js app to AWS with AWS.Website.Nextjs — the OpenNext serverless topology with streaming SSR, image optimization, and ISR wiring, plus next dev under alchemy dev.
4sidebar:
5 order: 9
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks @opennextjs/aws";
11
12`AWS.Website.Nextjs` deploys a [Next.js](https://nextjs.org) app to
13AWS using the [OpenNext](https://opennext.js.org/aws) serverless
14topology:
15
16- the Next server on a Lambda Function URL with response streaming
17- static assets and the prerender cache in S3, served through
18 CloudFront's edge router
19- a dedicated image-optimization Lambda behind `/_next/image`
20- ISR wiring: an SQS revalidation queue with a consumer Lambda and a
21 DynamoDB tag-cache table
22
23App Router and Pages Router both work — server components, API
24routes, middleware, server actions, dynamic segments, streaming SSR,
25and `getServerSideProps` pages all run in the Lambda.
26
27## Install
28
29The build integration is not bundled with alchemy. Install
30`@alchemy.run/frontend-frameworks` and its OpenNext peer; the
31resource loads the package's `/nextjs/aws` export from your project
32at deploy time. They are only used at build time, so dev dependencies
33are enough:
34
35<Tabs syncKey="pkgManager">
36 <TabItem label="bun" icon="bun">
37 <Code code={`bun add -d ${devDeps}`} lang="sh" />
38 </TabItem>
39 <TabItem label="npm" icon="npm">
40 <Code code={`npm install -D ${devDeps}`} lang="sh" />
41 </TabItem>
42 <TabItem label="pnpm" icon="pnpm">
43 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
44 </TabItem>
45 <TabItem label="yarn" icon="seti:yarn">
46 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
47 </TabItem>
48</Tabs>
49
50## Configure OpenNext
51
52Your `next.config.*` is loaded and honored as-is — the build runs a
53real `next build`, and Alchemy never rewrites the file. An
54`open-next.config.ts` is optional: when the project has none, a
55minimal default with the streaming server wrapper is generated for
56you. Keep your own file next to `next.config.*` to customize the
57OpenNext build:
58
59```typescript
60// open-next.config.ts
61export default {
62 default: {
63 override: {
64 wrapper: "aws-lambda-streaming",
65 },
66 },
67};
68```
69
70## Declare the Website
71
72Declare the site as a module-level const (rather than inline in the
73Stack):
74
75```typescript
76// alchemy.run.ts
77import * as AWS from "alchemy/AWS";
78
79export const Website = AWS.Website.Nextjs("Website");
80```
81
82## Add it to the Stack
83
84Yield the site from your Stack and return its URL:
85
86```typescript
87// alchemy.run.ts
88import * as Alchemy from "alchemy";
89import * as Effect from "effect/Effect";
90
91export default Alchemy.Stack(
92 "MyNextjsSite",
93 {
94 providers: AWS.providers(),
95 state: AWS.state(),
96 },
97 Effect.gen(function* () {
98 const site = yield* Website;
99 return { url: site.url };
100 }),
101);
102```
103
104Statically-rendered pages are served from the S3 prerender cache;
105dynamic pages and route handlers stream from the server Lambda;
106`next/image` requests are handled by the dedicated optimizer.
107
108See
109[examples/aws-website-nextjs](https://github.com/alchemy-run/alchemy/tree/main/examples/aws-website-nextjs)
110for the checked-in example.
111
112## Add environment variables
113
114The server function's environment is configured under `env` — plain
115values, or outputs from other resources in the Stack:
116
117```diff lang="typescript"
118// alchemy.run.ts
119export const Website = AWS.Website.Nextjs("Website", {
120+ env: {
121+ GREETING: "Hello from Alchemy!",
122+ API_BASE: api.url,
123+ },
124});
125```
126
127The values are set on the server Lambda at deploy time and injected
128into the dev server's process environment under `alchemy dev`, so
129server code reads the same values in both modes.
130
131## Read the environment in server code
132
133The OpenNext server runs in a plain Node Lambda, so route handlers,
134server components, and server actions read the environment from
135`process.env`:
136
137```typescript
138// app/api/hello/route.ts
139export function GET() {
140 return Response.json({ greeting: process.env.GREETING ?? "hello" });
141}
142```
143
144## ISR
145
146Incremental Static Regeneration works out of the box — the resource
147provisions the OpenNext cache infrastructure for you:
148
149- the ISR/fetch cache lives in a dedicated S3 bucket
150- `revalidatePath` / `revalidateTag` purge entries through the
151 DynamoDB tag-cache table
152- time-based `revalidate` windows regenerate pages in the background
153 through the SQS FIFO revalidation queue and its consumer Lambda
154
155There is no cache configuration to write — the resource wires
156OpenNext's standard cache environment (`CACHE_BUCKET_NAME`,
157`CACHE_DYNAMO_TABLE`, `REVALIDATION_QUEUE_URL`) onto the server
158Lambda automatically, so OpenNext's default S3 / DynamoDB / SQS
159overrides just work.
160
161## Local dev
162
163```sh
164bun alchemy dev
165```
166
167`alchemy dev` runs `next dev` (native HMR, Turbopack) instead of
168deploying; `site.url` is the local address and no AWS resources are
169created. Wrap the site in `Alchemy.remote()` to deploy the real
170infrastructure even during dev.
171
172## Custom domain
173
174```typescript
175const site = yield* AWS.Website.Nextjs("Web", {
176 domain: { name: "app.example.com" },
177});
178```
179
180The hosted zone is inferred from the hostname. Pass `hostedZoneId` to
181pin it when several zones could match.