Skip to main content

Overview

Fjall builds Next.js with OpenNext, 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:
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, and a server-rendered Payload application through the Payload pattern, which is the OpenNext construct that exists.

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.

Detection signals

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

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: 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:
--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: Everything else about that route (clean URLs, security headers, contact forms, access gates) lives on the static-site pattern page.

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

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. All three Lambdas run Node.js 24 on ARM64.

CloudFront routing

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:
That URL is the CloudFront distribution domain. It is live as soon as the deploy reports success.

Redeploy

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

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.

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.
The config schema is strict. An unknown key fails validation before synth.
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.
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.

Troubleshooting

fjall create app --pattern nextjs is refused

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:

Cannot find @opennextjs/aws package

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

Deploy

Every flag on fjall deploy, plus targets and the approval gate

Static Site Pattern

The route for a Next.js static export

Payload Pattern

Payload on OpenNext, with the full configuration reference

Custom Domain

Certificates, hosted zones and alias records

Add Resources

Extend the app with more AWS services

Create Application

Scaffolding options and tiers
Fjall: External: