Skip to content

Next.js

AWS.Website.Nextjs deploys a Next.js app to AWS using the OpenNext serverless topology:

  • the Next server on a Lambda Function URL with response streaming
  • static assets and the prerender cache in S3, served through CloudFront’s edge router
  • a dedicated image-optimization Lambda behind /_next/image
  • ISR wiring: an SQS revalidation queue with a consumer Lambda and a DynamoDB tag-cache table

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 Lambda.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks and its OpenNext peer; the resource loads the package’s /nextjs/aws export from your project at deploy time. They are only used at build time, so dev dependencies are enough:

Terminal window
bun add -d @alchemy.run/frontend-frameworks @opennextjs/aws

Your next.config.* is loaded and honored as-is — the build runs a real next build, and Alchemy never rewrites the file. An open-next.config.ts is optional: when the project has none, a minimal default with the streaming server wrapper is generated for you. Keep your own file next to next.config.* to customize the OpenNext build:

open-next.config.ts
export default {
default: {
override: {
wrapper: "aws-lambda-streaming",
},
},
};

Declare the site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as AWS from "alchemy/AWS";
export const Website = AWS.Website.Nextjs("Website");

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyNextjsSite",
{
providers: AWS.providers(),
state: AWS.state(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

Statically-rendered pages are served from the S3 prerender cache; dynamic pages and route handlers stream from the server Lambda; next/image requests are handled by the dedicated optimizer.

See examples/aws-website-nextjs for the checked-in example.

The server function’s environment is configured under server.environment — plain values, or outputs from other resources in the Stack:

alchemy.run.ts
export const Website = AWS.Website.Nextjs("Website", {
server: {
environment: {
GREETING: "Hello from Alchemy!",
API_BASE: api.url,
},
},
});

The values are set on the server Lambda at deploy time and injected into the dev server’s process environment under alchemy dev, so server code reads the same values in both modes.

The OpenNext server runs in a plain Node Lambda, so route handlers, server components, and server actions read the environment from process.env:

app/api/hello/route.ts
export function GET() {
return Response.json({ greeting: process.env.GREETING ?? "hello" });
}

Incremental Static Regeneration works out of the box — the resource provisions the OpenNext cache infrastructure for you:

  • the ISR/fetch cache lives in a dedicated S3 bucket
  • revalidatePath / revalidateTag purge entries through the DynamoDB tag-cache table
  • time-based revalidate windows regenerate pages in the background through the SQS FIFO revalidation queue and its consumer Lambda

There is no cache configuration to write — the resource wires OpenNext’s standard cache environment (CACHE_BUCKET_NAME, CACHE_DYNAMO_TABLE, REVALIDATION_QUEUE_URL) onto the server Lambda automatically, so OpenNext’s default S3 / DynamoDB / SQS overrides just work.

Terminal window
bun alchemy dev

alchemy dev runs next dev (native HMR, Turbopack) instead of deploying; site.url is the local address and no AWS resources are created. Wrap the site in Alchemy.remote() to deploy the real infrastructure even during dev.

const site = yield* AWS.Website.Nextjs("Web", {
domain: {
name: "app.example.com",
hostedZoneId: zone.hostedZoneId,
},
});