> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fjall.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js Pattern

> How Fjall deploys Next.js on AWS: OpenNext Lambda bundles behind CloudFront, or a static export on S3. Detection, architecture, and next steps.

## Overview

Fjall builds Next.js with [OpenNext](https://opennext.js.org/), which compiles a
Next.js app into Lambda bundles plus a folder of static assets. Two entries in
Fjall's pattern registry produce that artefact:

| Pattern   | Label       | Build artefact    | Status                                |
| --------- | ----------- | ----------------- | ------------------------------------- |
| `payload` | Payload CMS | `opennext-lambda` | Deployable                            |
| `nextjs`  | Next.js     | `opennext-lambda` | Declared, no infrastructure construct |

<Warning>
  The `nextjs` pattern has no CDK construct. `IPatternProps` omits it,
  `PatternFactory.build` refuses it, and the registry marks it undeployable, so
  `fjall create app --pattern nextjs` is rejected before anything is written.

  Two routes to AWS work today: a **static export** through the
  [static-site pattern](/patterns/static-site-pattern), and a **server-rendered
  Payload application** through the [Payload pattern](/patterns/payload-pattern),
  which is the OpenNext construct that exists.
</Warning>

## How Fjall recognises a Next.js app

`fjall apps detect <path>` reads each workspace's `package.json` and names the
framework from its dependencies. Both `dependencies` and `devDependencies`
count.

```bash theme={null}
fjall apps detect ~/code/storefront
fjall apps detect ~/code/storefront --output json
```

### Detection signals

| Framework        | Dependencies that prove it | Checked |
| ---------------- | -------------------------- | ------- |
| `nextjs+payload` | `next` and `payload`       | First   |
| `nextjs`         | `next`                     | Second  |
| `payload`        | `payload`                  | Third   |

`nextjs+payload` is checked first because it is the strict superset. Payload 3
ships as a Next.js plugin, so both dependencies appear in a Payload repository,
and matching `nextjs` first would misclassify every Payload app.

Two conventions follow from the framework name:

| Convention             | Value for `nextjs` |
| ---------------------- | ------------------ |
| Default port           | `3000`             |
| Build output directory | `out`              |

<Note>
  `out` is a convention, not an observation. Detection never runs the build.
  Confirm the directory exists before deploying a static export, or the deploy
  succeeds onto an empty bucket.
</Note>

### What detection recommends

Detection reports a framework. `--pattern` takes a pattern. The two vocabularies
are joined by one table, and the verdict says what to do next:

| Detected         | Verdict     | What to run                                             |
| ---------------- | ----------- | ------------------------------------------------------- |
| `nextjs+payload` | Recommended | `--pattern payload`                                     |
| `payload`        | Recommended | `--pattern payload`                                     |
| `nextjs`         | Blocked     | A static export with `--pattern staticsite`, or Payload |

For a recommended pattern, the JSON output also carries the exact `--source`,
`--build-command` and `--output-dir` values to pass to `fjall create app`. A
blocked framework carries no prefill, because there is nothing to create.

## Choosing the route

### Statically exported Next.js

A `next.config.js` carrying `output: 'export'` writes a folder of HTML, CSS and
JavaScript into `out/`. That folder is exactly what the static-site pattern
serves:

```bash theme={null}
fjall create app \
  --name web \
  --pattern staticsite \
  --source ../.. \
  --build-command "npm run build" \
  --output-dir out \
  --routing multipage
```

`--source` is relative to the app's `fjall/web` config directory, so `../..` is
the repository root. `--output-dir` is relative to `--source`.

Pick the routing mode from what your build writes:

| Mode        | `/about` serves       | Use when                                    |
| ----------- | --------------------- | ------------------------------------------- |
| `multipage` | `about.html`          | Default Next.js export layout               |
| `directory` | `about/index.html`    | Your build writes a directory per page      |
| `spa`       | `index.html` fallback | Client-side routing owns every unknown path |

Everything else about that route (clean URLs, security headers, contact forms,
access gates) lives on the [static-site pattern page](/patterns/static-site-pattern).

### Server-rendered Next.js

Server rendering, API routes and `next/image` need the OpenNext Lambda shape.
The only construct that builds it is the Payload pattern, and its deploy runs
`npx payload generate:importmap` before the OpenNext build. It fits a Next.js
app that **is** a Payload application:

```bash theme={null}
fjall create app \
  --name cms \
  --pattern payload \
  --tier standard \
  --non-interactive
```

A Next.js app with no Payload dependency has no server-rendered route today.
Deploy it as a static export, or wait for the `nextjs` construct.

<Note>
  `--pattern` and `--tier` are honoured only on the non-interactive path. On a
  TTY, `fjall create app` opens the tier wizard, which has no pattern step. Run
  bare `fjall create` and choose **Patterns** instead, where the picker lists
  only the deployable patterns: Payload CMS and Static site.
</Note>

## What an OpenNext deploy provisions

Read from the Payload construct, which is the OpenNext pattern that synthesises.
The database and the media bucket are Payload's. The rest is the OpenNext shape
any Next.js pattern needs.

| Resource                  | Purpose                                         | Built from                               |
| ------------------------- | ----------------------------------------------- | ---------------------------------------- |
| **Lambda (server)**       | SSR, API routes, admin panel                    | `.open-next/server-functions/default`    |
| **Lambda (image)**        | `next/image` optimisation                       | `.open-next/image-optimization-function` |
| **Lambda (revalidation)** | Drains the ISR revalidation queue               | `.open-next/revalidation-function`       |
| **S3 assets bucket**      | Static build output, pruned on every deploy     | `.open-next/assets`                      |
| **S3 cache bucket**       | ISR cache, kept between deploys                 | `.open-next/cache`                       |
| **DynamoDB table**        | Tag-to-path cache for ISR revalidation          | Created empty                            |
| **SQS FIFO queue**        | Revalidation messages, with a dead-letter queue | Created empty                            |
| **CloudFront**            | HTTPS, path routing, edge caching               | Fronts the Lambdas and the assets bucket |
| **RDS PostgreSQL**        | Payload's database (Aurora or Instance)         | Payload only                             |
| **S3 media bucket**       | Payload uploads                                 | Payload only                             |

All three Lambdas run Node.js 24 on ARM64.

### CloudFront routing

| Path pattern      | Origin        | Cache policy        |
| ----------------- | ------------- | ------------------- |
| `/_next/static/*` | Assets bucket | `CACHING_OPTIMIZED` |
| `/_next/image*`   | Image Lambda  | `CACHING_OPTIMIZED` |
| `/api/*`          | Server Lambda | `CACHING_DISABLED`  |
| `/admin/*`        | Server Lambda | `CACHING_DISABLED`  |
| everything else   | Server Lambda | `CACHING_DISABLED`  |

Assets are uploaded with a one-year immutable `Cache-Control`, which is safe
because Next.js content-hashes their filenames. The distribution defaults to
`PriceClass_100`.

### The build step

Before synthesis, Fjall runs `open-next build`, resolving the `open-next` binary
from the project's own `node_modules` rather than from the CLI's. The build has
a 10-minute ceiling. On failure Fjall deletes `.open-next/` so a later synth
cannot pick up a half-written bundle, and a build that exits 0 without writing
`.open-next/` is reported as a failure rather than deployed.

## After a successful deploy

The deploy ends with the block that brought you here:

```text theme={null}
Next Steps:
  Visit your site: https://d111111abcdef8.cloudfront.net
  Next.js documentation: https://nextjs.org/docs
  Learn more: https://docs.fjall.io/patterns/nextjs
```

That URL is the CloudFront distribution domain. It is live as soon as the deploy
reports success.

### Redeploy

```bash theme={null}
fjall deploy web
```

The positional argument is the application name. Run `fjall deploy` bare on a
terminal for the menu instead.

When nothing has changed, the run ends with `Already up to date — no changes
since last deploy.` and touches nothing. Pass `-f` / `--force` to redeploy every
stack anyway:

```bash theme={null}
fjall deploy web --force
```

In CI, `--skip-confirmation` skips the deployment confirmation prompts and
`--auto-approve` satisfies the approval gate.

Neither reaches the destruction gate, which is separate and unconditional. A
replacement that loses data, a replacement of a custom-named resource, or a
delete of a stateful resource raises a ticket that only exact, case-sensitive
`{physical name, verb}` consents clear. `--force`, `--skip-confirmation`,
`--auto-approve` and approval tokens never satisfy it.

### Add resources

The pattern is a composition of the same factories you can call directly. Add a
queue, a bucket or a second database with `fjall add`, or edit
`fjall/web/infrastructure.ts` by hand. See [Add Resources](/deployment/add-resources).

### Point a domain at it

Set `domain` on the pattern statement and Fjall creates the ACM certificate and
the Route53 alias record. Full walkthrough, including bring-your-own certificate
and delegated zones: [Custom Domain](/deployment/custom-domain).

## Configuration

Pattern configuration lives in `fjall/<app>/infrastructure.ts`, in the second
argument to `PatternFactory.build`. The root `fjall-config.json` holds
project-level state (active target, domains, organisation binding) and carries
no pattern configuration.

```typescript theme={null}
app.addPattern(
  PatternFactory.build("CmsPayload", {
    type: "payload",
    name: "cms",
    domain: "cms.example.com",
    compute: {
      server: { memorySize: 2048, timeout: 60 },
    },
  }),
);
```

The config schema is strict. An unknown key fails validation before synth.

<Warning>
  The first argument is a derived construct id: `toPascalCase(name)` plus the
  pattern's suffix, so `cms` yields `CmsPayload` and would yield `CmsNextjs`.
  `fjall modify pattern` and `fjall remove pattern` locate the statement by that
  id. Rename it by hand and the CLI can no longer find the pattern.
</Warning>

Both OpenNext patterns take the same option set: `type`, `name`, `domain`,
`database`, `compute`, `storage`, `messaging`, `cdn` and `environment`. The
`nextjs` variant differs in one respect, `database` being optional rather than
always provisioned, and the deploy reads the pattern's own `database:` block to
decide whether a database stack is part of the app. Every option is documented
against the live construct on the
[Payload pattern page](/patterns/payload-pattern#configuration-reference).

## Troubleshooting

### `fjall create app --pattern nextjs` is refused

```text theme={null}
The "nextjs" pattern is not yet deployable — its infrastructure construct is not
implemented. Available patterns: payload, staticsite. For a Next.js-based app,
use --pattern payload.
```

Expected. Take one of the two routes above.

### `"nextjs+payload" is a framework, not a pattern`

`fjall apps detect` reports a **framework**. `--pattern` takes a **pattern**. The
error names the mapping:

```text theme={null}
"nextjs+payload" is a framework, not a pattern. Payload v3 ships as a Next.js
plugin, so a repository carrying both dependencies is a Payload application and
deploys as one. Use --pattern payload. Valid patterns: payload, staticsite.
```

### `Cannot find @opennextjs/aws package`

```bash theme={null}
npm install @opennextjs/aws
```

Fjall resolves the binary from the project's own `node_modules`, so a global
install does not satisfy it.

### `OpenNext build timed out after 600 seconds`

The build exceeded its 10-minute ceiling. Run `open-next build` yourself to see
where the time goes, then trim it. Cold CI machines installing dependencies
inside the build step are the common cause.

### `OpenNext build completed but .open-next directory not found`

The build exited 0 and wrote nothing. Run `open-next build` in the project and
confirm `.open-next/` appears with `server-functions/default`, `assets` and
`cache` inside it.

### A static export deploys to an empty site

`--output-dir` does not match what the build actually wrote. Build first, look
at the directory, then correct the value in `fjall/<app>/infrastructure.ts`.
Nothing in detection verifies this against disk.

## Next steps

<CardGroup cols={2}>
  <Card title="Deploy" icon="rocket" href="/cli/deploy">
    Every flag on `fjall deploy`, plus targets and the approval gate
  </Card>

  <Card title="Static Site Pattern" icon="globe" href="/patterns/static-site-pattern">
    The route for a Next.js static export
  </Card>

  <Card title="Payload Pattern" icon="cube" href="/patterns/payload-pattern">
    Payload on OpenNext, with the full configuration reference
  </Card>

  <Card title="Custom Domain" icon="globe" href="/deployment/custom-domain">
    Certificates, hosted zones and alias records
  </Card>

  <Card title="Add Resources" icon="plus" href="/deployment/add-resources">
    Extend the app with more AWS services
  </Card>

  <Card title="Create Application" icon="plus" href="/deployment/create-application">
    Scaffolding options and tiers
  </Card>
</CardGroup>

## Related documentation

**Fjall:**

* [Compute Factory](/patterns/compute-factory) - Lambda configuration
* [Storage Factory](/patterns/storage-factory) - S3 bucket options
* [CDN Factory](/patterns/cdn-factory) - CloudFront behaviours
* [Database Factory](/patterns/database-factory) - Database properties

**External:**

* [Next.js docs](https://nextjs.org/docs) - Official Next.js documentation
* [Static exports](https://nextjs.org/docs/app/guides/static-exports) - `output: 'export'`
* [OpenNext](https://opennext.js.org/) - Next.js to AWS Lambda
