Overview
The Payload pattern deploys a Payload CMS application to AWS Lambda. Payload 3.x runs on Next.js, so Fjall builds it with OpenNext and fronts it with CloudFront. One pattern statement provisions the whole stack:- Serverless compute: three ARM64 Lambda functions (server, image, revalidation)
- Managed PostgreSQL: RDS Instance by default, Aurora Serverless v2 on the Resilient tier
- S3 storage: three buckets for build assets, ISR cache, and media uploads
- CloudFront CDN: HTTPS, path-based routing, and edge caching
- Automatic patching: the CLI rewrites your Payload project for Lambda at deploy time
Prerequisites
Quick start
1. Scaffold a Payload project
Start with the official website template:2. Create the Fjall application
fjall/my-payload-site/infrastructure.ts plus the application’s config file. The scaffold directory is named after --name.
3. Deploy
Choosing a pattern
The interactive picker offers two patterns:A
nextjs pattern exists in the registry but has no infrastructure
construct, so it is not offered in the picker and --pattern nextjs is
refused at create time. For a Next.js application that needs a server, use
--pattern payload. For a static export, use --pattern staticsite.Choosing a tier
Pattern applications have four tiers, prompted as Choose your configuration tier?:
Standard retains backups for 14 days with deletion protection on. Lightweight retains 7 days with deletion protection off. Resilient retains 30 days with customer-managed KMS encryption.
Custom replaces the preset with seven prompts: database type, instance type (Instance only), backup retention, deletion protection, KMS encryption (Aurora only), Lambda memory, and Lambda timeout.
The six-option list (Standard, Lightweight, Resilient, Enterprise, Tinkerer,
Custom) belongs to the plain application wizard reached with
-t/--type.
Pattern applications have four tiers and no Enterprise or Tinkerer option.What the pattern provisions
CloudFront routes four path patterns:
The generated infrastructure file
fjall/my-payload-site/infrastructure.ts looks like this:
database and compute blocks are always written out, even when you accept every default, so the file shows you what you are getting rather than hiding it behind the pattern. storage, messaging, cdn and environment are the opposite: they appear only once you configure them. The values above are the Standard tier, which is also what set network and backup on App.getApp. Only server picks up the tier’s Lambda sizing; imageOptimisation and revalidation keep the construct defaults on every tier.
Edit the second argument to configure the pattern. See Configuration reference below.
What the CLI patches
fjall deploy rewrites the Payload project for Lambda before it builds. Every patch is idempotent, so repeated deploys are safe, and the CLI skips any file it does not find.
Packages
Installs@fjall/payload and @payloadcms/storage-s3.
Database adapter
Replaces the Postgres adapter inpayload.config.ts and spreads Fjall’s CORS and CSRF defaults into buildConfig:
fjallDefaults() locks CORS and CSRF to NEXT_PUBLIC_SERVER_URL when a custom domain is set, and stays permissive otherwise.
S3 media storage
Adds Fjall’s S3 plugin tosrc/plugins/index.ts:
awsS3Storage() reads MEDIA_BUCKET_NAME and AWS_REGION from the Lambda environment and falls back to disk storage locally. It also patches src/collections/Media.ts to drop staticDir in production.
Next.js configuration
Addsoutput: 'standalone' for OpenNext and an outputFileTracingIncludes block that traces runtime dependencies Lambda would otherwise miss under pnpm.
Build-time guards
Payload pages that query the database duringnext build fail without a database connection. The CLI guards the website template’s pages:
These paths track the Payload website template layout. In a project with a different structure, guard your own build-time queries with
isBuildTime() from @fjall/payload:
Deploying
fjall deploy opens an application picker. The positional argument is the application name, not a tier and not a domain.
Pre-flight checks
The first deploy streams a checklist before any AWS call:- Detecting application pattern
- Configuring Payload for AWS
- Creating database migrations
- Generating Payload import map
- Building Next.js application
- Synthesising infrastructure
- Comparing template hashes
- Validating Docker configuration
- Validating SSM secrets
Approval gate
With changes pending, Fjall prints a deployment plan and asks Approve this plan and deploy?, defaulting to No. Rejecting the plan changes nothing. Pass--skip-confirmation or --auto-approve in CI.
Resources marked for replacement or deletion trigger a separate typed-name consent step that no flag bypasses.
The first deploy takes roughly 10 to 15 minutes because CloudFormation creates the VPC, database, and distribution. Later deploys update only what changed. If nothing changed, the run ends with Already up to date.
After the deploy
Find your URL
The CDN stack exports the distribution domain as<PatternName>DistributionDomainName. Read it from the deploy output or from the application status:
- Frontend:
https://d1234567890.cloudfront.net - Admin panel:
https://d1234567890.cloudfront.net/admin
Create the first admin user
- Open
/admin - The first-run screen prompts for the initial user
- Enter an email address and password
- Sign in
Verify the deployment
- Admin panel loads at
/admin - First admin user can be created
- Media uploads land in S3
- Content can be created and published
- Published content renders on the frontend
- Images load through
/_next/image
Database migrations
Payload manages schema changes with migration files.Generate a migration
Apply migrations
Migrations run on Lambda cold start viaprodMigrations, which the CLI wires into the adapter for you:
Environment variables
Fjall injects these into the server Lambda. Do not set them yourself.
Database credentials come from Secrets Manager, never from an environment variable. Add your own variables through the pattern’s
environment block, which merges over the injected set.
Custom domain
Setdomain and Fjall creates the ACM certificate and the Route53 alias record:
cdn block:
us-east-1 for CloudFront. Setting a domain also sets NEXT_PUBLIC_SERVER_URL, which locks Payload’s CORS and CSRF to that origin.
Configuration reference
Root options
The config schema is strict. An unknown key fails validation before synth.
Database options
Aurora adds
writer and readers. Instance adds readReplica. See the DatabaseFactory reference for the full property set.
Compute options
Each ofserver, imageOptimisation, and revalidation accepts the same three keys.
The defaults above are the construct’s. A tier preset overrides the server memory and timeout at create time, so a Standard application is written with 1536 MB and 60 seconds.
Storage options
The media bucket follows the application’s backup tier, so versioning is on at Resilient and Enterprise. Assets and cache stay unversioned at every tier because both churn, and each carries the
fjall:s3:versioning-exempt tag saying so (build-output and isr-cache), so a posture scan reads the choice as deliberate; versioned: true drops the tag. An explicit value always wins:
Messaging options
CDN options
Prefer the root-level
domain for a simple setup. The cdn block is for supplying a certificate you already manage.
Full example
Going beyond the pattern
The pattern is a composition of the same factories you can call directly. Replace the pattern statement with individual factory calls when you need to:- Add resources the pattern does not create, such as a worker queue or Redis
- Customise networking, for example VPC peering or private subnets
- Share a database between several applications
- Narrow IAM permissions past the pattern’s defaults
fjall add or by editing the file directly. See Add Resources.
Troubleshooting
Build fails with “Error occurred prerendering page”
A page queries the database duringnext build. The CLI guards the website template’s pages automatically, so first update the CLI:
isBuildTime() from @fjall/payload.
403 on admin panel actions
Payload’s CSRF list does not include the origin you are calling from.fjallDefaults() sets cors and csrf correctly, so keep the spread first in buildConfig and do not redeclare either key after it:
Media uploads disappear
The Media collection is still writing to the local filesystem. KeepawsS3Storage() in the plugins array and keep staticDir behind isProduction() in src/collections/Media.ts.
”relation does not exist”
Migrations were never generated or never committed:fjall modify pattern cannot find the pattern
The construct id in PatternFactory.build was renamed. Restore it to toPascalCase(name) plus Payload, so my-payload-site becomes MyPayloadSitePayload.
Costs
The database and the NAT gateway are always-on, so they set the floor. Lambda, S3 and CloudFront are usage-priced and stay small on a low-traffic site. Indicative us-east-1 on-demand list prices, excluding data transfer:Next Steps
Deploy Application
Flags, targets, and the approval gate
Custom Domain
Point a domain at your CloudFront distribution
Add Resources
Extend the application with more AWS services
Database Factory
Every database property the pattern exposes
Static Site Pattern
Deploy a pre-built folder instead
CI/CD
Automate deployments with Buildkite
Related documentation
Fjall:- Create Application - Scaffolding options
- Compute Factory - Lambda and ECS configuration
- Storage Factory - S3 bucket options
- CDN Factory - CloudFront behaviours
- Network Factory - VPC and networking
- Payload CMS docs - Official Payload documentation
- OpenNext - Next.js to AWS Lambda
- Aurora Serverless v2 - AWS Aurora documentation