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

# Payload CMS Pattern

> Deploy Payload CMS to AWS Lambda with Fjall's managed PostgreSQL, S3, and CloudFront infrastructure.

## Overview

The Payload pattern deploys a production-ready [Payload CMS](https://payloadcms.com/) application to AWS Lambda with:

* **Serverless compute** - Lambda functions on ARM64 for cost efficiency
* **Managed PostgreSQL** - RDS PostgreSQL (single instance by default, Aurora Serverless v2 optional)
* **S3 storage** - media uploads and ISR cache
* **CloudFront CDN** - global distribution with HTTPS
* **Automatic patching** - Fjall configures the project for Lambda deployment

<Note>
  Payload CMS 3.x is built on Next.js, so it deploys to Lambda via OpenNext.
</Note>

***

## Quick Start

### 1. Create a Payload Project

Start with the official Payload website template:

```bash theme={null}
npx create-payload-app@latest my-payload-site --template website
cd my-payload-site
```

### 2. Initialise Fjall

```bash theme={null}
fjall create app --name my-payload-site --pattern payload
```

This creates the `fjall/app/` directory with your infrastructure configuration.

### 3. Deploy

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

Fjall patches the project for Lambda, builds it, and provisions the infrastructure.

***

## Scaffolding Payload

Payload CMS offers several official templates. Choose based on your needs:

### Website Template (Recommended)

Full-featured website with pages, posts, and media:

```bash theme={null}
npx create-payload-app@latest my-site --template website
```

Includes:

* Pages collection with dynamic routing
* Posts collection with categories
* Media collection
* Header/Footer globals
* Draft/publish workflow
* SEO fields

### Blank Template

Minimal starting point:

```bash theme={null}
npx create-payload-app@latest my-site --template blank
```

Includes:

* Users collection only
* No pre-built collections
* Clean slate for custom schemas

### E-commerce Template

Full e-commerce setup:

```bash theme={null}
npx create-payload-app@latest my-store --template ecommerce
```

Includes:

* Products, Categories, Orders
* Cart functionality
* Stripe integration ready

### Interactive Setup

Or let Payload guide you:

```bash theme={null}
npx create-payload-app@latest
```

You'll be prompted for:

* Project name
* Template selection
* Database type (choose PostgreSQL for Fjall)
* Package manager

<Tip>
  Choose **PostgreSQL** as your database when scaffolding. Fjall provisions RDS
  PostgreSQL for the Payload pattern.
</Tip>

***

## Creating Your Fjall App

### Basic Setup

After scaffolding Payload, initialise Fjall:

```bash theme={null}
cd my-payload-site
fjall create app --name my-payload-site --pattern payload
```

`payload` is the only supported pattern. The interactive picker also lists a
Next.js option, but it is experimental.

<Warning>
  The `nextjs` pattern is experimental and not yet deployable. The PatternFactory
  builds only `payload`, so selecting Next.js fails at deploy time. Use `payload`.
</Warning>

Running `fjall create app` interactively asks **Choose your configuration
tier?** with six options: Standard, Lightweight, Resilient, Enterprise,
Tinkerer, and Custom. The Custom tier opens a sub-flow for database type, backup
retention, deletion protection, KMS encryption, and Lambda memory/timeout.

This creates:

```
my-payload-site/
├── fjall/
│   └── app/
│       ├── infrastructure.ts    # Your infrastructure definition
│       └── fjall.config.json    # App configuration
├── src/
│   └── ...                      # Your Payload source
└── package.json
```

### Generated Infrastructure

The default `infrastructure.ts`:

```typescript theme={null}
import { App, PatternFactory } from "@fjall/components-infrastructure";

const app = App.getApp("app");

export const infrastructure = app.addPattern(
  PatternFactory.build("App", {
    type: "payload",
    name: "my-payload-site",
  }),
);
```

This provisions all resources needed for production deployment.

***

## What's Included

The Payload pattern provisions:

| Resource                  | Purpose                                                 |
| ------------------------- | ------------------------------------------------------- |
| **RDS PostgreSQL**        | Payload database (Instance by default, Aurora optional) |
| **S3 Assets Bucket**      | Media uploads, static files                             |
| **S3 Cache Bucket**       | Next.js ISR cache                                       |
| **DynamoDB Table**        | Tag-to-path cache for ISR                               |
| **SQS FIFO Queue**        | Background revalidation                                 |
| **Lambda (Server)**       | SSR, API routes, admin panel                            |
| **Lambda (Image)**        | next/image optimisation                                 |
| **Lambda (Revalidation)** | Background ISR processing                               |
| **CloudFront**            | CDN with HTTPS, routing, caching                        |

***

## Automatic Configuration

When you run `fjall deploy`, the CLI automatically patches your Payload project:

### Database Adapter

Replaces your database adapter with `@fjall/payload`:

```typescript theme={null}
// Before
db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL } });

// After (automatic)
db: await fjallPostgresAdapter({ prodMigrations: migrations });
```

### S3 Storage

Adds the S3 storage plugin for media uploads:

```typescript theme={null}
plugins: [
  s3Storage({
    collections: { media: { prefix: "media" } },
    bucket: process.env.MEDIA_BUCKET_NAME,
  }),
];
```

### Next.js Configuration

Configures Next.js for Lambda:

* `output: 'standalone'` for OpenNext
* File tracing for Sharp (ARM64 binaries)
* Exclusions for unnecessary files

### Build-Time Fixes

Patches page files to prevent database calls during build:

| Page                    | Fix Applied                      |
| ----------------------- | -------------------------------- |
| `[slug]/page.tsx`       | Guard returns `[]` at build time |
| `posts/[slug]/page.tsx` | Guard returns `[]` at build time |
| `page.tsx` (home)       | Uses `force-dynamic`             |
| `posts/page.tsx`        | Uses `force-dynamic`             |
| `search/page.tsx`       | Uses `force-dynamic`             |

<Info>
  All patches are idempotent. Running `fjall deploy` multiple times is safe.
</Info>

***

## Deployment Process

### First Deploy

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

The first deployment:

1. Installs `@fjall/payload` and `@payloadcms/storage-s3`
2. Patches configuration files
3. Generates OpenNext config
4. Builds with `next build`
5. Deploys infrastructure (\~10-15 minutes)

### Subsequent Deploys

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

Updates are faster as infrastructure exists. Only changed resources update.

***

## Post-Deployment

### Access Your Site

After deployment, you'll see:

```bash theme={null}
Outputs:
  CloudFrontURL: https://d1234567890.cloudfront.net
```

* **Frontend**: `https://d1234567890.cloudfront.net`
* **Admin Panel**: `https://d1234567890.cloudfront.net/admin`

### Create First Admin User

1. Navigate to `/admin`
2. The first-run screen prompts you to create the initial user
3. Enter an email and password
4. Sign in to the admin panel

### Verify Everything Works

* [ ] Admin panel loads at `/admin`
* [ ] Can create first admin user
* [ ] Can upload media (stored in S3)
* [ ] Can create/publish content
* [ ] Frontend displays published content
* [ ] Images load correctly

***

## Database Migrations

Payload uses migrations to manage database schema changes.

### Generate Migrations

When you change your collections:

```bash theme={null}
npx payload migrate:create
```

This creates migration files in `src/migrations/`.

### Deploy Migrations

Migrations run automatically on Lambda cold start via `prodMigrations`:

```typescript theme={null}
db: await fjallPostgresAdapter({
  prodMigrations: migrations, // Runs on first request
  migrationDir: "./src/migrations",
});
```

<Warning>
  Always generate and commit migrations before deploying schema changes.
</Warning>

***

## Environment Variables

Fjall automatically configures these Lambda environment variables:

### Database

```bash theme={null}
DATABASE_HOST=your-db-endpoint.rds.amazonaws.com
DATABASE_PORT=<assigned-port>   # Fjall may assign a non-default port
DATABASE_NAME=payload
DATABASE_SSL=true
# Username and password fetched from Secrets Manager
```

### OpenNext Cache

```bash theme={null}
CACHE_BUCKET_NAME=your-cache-bucket
CACHE_DYNAMO_TABLE=your-tag-cache-table
REVALIDATION_QUEUE_URL=https://sqs.region.amazonaws.com/...
```

### Media Storage

```bash theme={null}
MEDIA_BUCKET_NAME=your-assets-bucket
```

***

## Custom Domain

To use a custom domain:

```typescript theme={null}
// fjall/app/infrastructure.ts
import { App, PatternFactory } from "@fjall/components-infrastructure";

const app = App.getApp("app");

export const infrastructure = app.addPattern(
  PatternFactory.build("App", {
    type: "payload",
    name: "my-payload-site",
    domain: "cms.yourdomain.com", // Automatically creates certificate and DNS
  }),
);
```

Or with manual certificate configuration:

```typescript theme={null}
export const infrastructure = app.addPattern(
  PatternFactory.build("App", {
    type: "payload",
    name: "my-payload-site",
    cdn: {
      domainNames: ["cms.yourdomain.com"],
      certificateArn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123",
    },
  }),
);
```

<Note>
  The certificate must be in `us-east-1` for CloudFront.
</Note>

***

## Pattern Parameters

The Payload pattern accepts configuration options for each component:

### Full Configuration Example

```typescript theme={null}
import { App, PatternFactory } from "@fjall/components-infrastructure";

const app = App.getApp("app");

export const infrastructure = app.addPattern(
  PatternFactory.build("App", {
    type: "payload",
    name: "my-payload-site",

    // Simple domain setup (auto-creates certificate)
    domain: "cms.example.com",

    // Database configuration
    database: {
      databaseName: "payload",
      deletionProtection: true,
      databaseInsights: {
        retentionPeriod: 7,
      },
    },

    // Lambda configuration
    compute: {
      server: {
        memorySize: 1536, // MB for server function
        timeout: 30, // Seconds
      },
      imageOptimisation: {
        memorySize: 1536, // For image processing
      },
    },

    // S3 configuration
    storage: {
      assets: { versioned: true },
      cache: { versioned: false },
      media: { versioned: true },
    },

    // Additional environment variables for server Lambda
    environment: {
      LOG_LEVEL: "info",
    },
  }),
);
```

### Root Options

| Option        | Type        | Default  | Description                                        |
| ------------- | ----------- | -------- | -------------------------------------------------- |
| `type`        | `"payload"` | Required | Pattern type                                       |
| `name`        | string      | Required | Application name                                   |
| `domain`      | string      | -        | Custom domain (auto-creates certificate and DNS)   |
| `environment` | object      | -        | Additional environment variables for server Lambda |

### Database Options

| Option                             | Type                     | Default      | Description                           |
| ---------------------------------- | ------------------------ | ------------ | ------------------------------------- |
| `type`                             | `"Instance" \| "Aurora"` | `"Instance"` | Database engine variant               |
| `databaseName`                     | string                   | App name     | Name of the database                  |
| `backupRetention`                  | number                   | `7`          | Days to retain automated backups      |
| `deletionProtection`               | boolean                  | `true`       | Prevent accidental deletion           |
| `databaseInsights`                 | object                   | -            | Database Insights configuration       |
| `databaseInsights.retentionPeriod` | number                   | `7`          | Days to retain database insights data |

<Note>
  See the [DatabaseFactory documentation](/patterns/database-factory) for the full list of available database parameters.
</Note>

### CDN Options

| Option           | Type      | Default | Description                                |
| ---------------- | --------- | ------- | ------------------------------------------ |
| `domainNames`    | string\[] | -       | Custom domain names                        |
| `certificateArn` | string    | -       | ACM certificate ARN (must be in us-east-1) |

### Compute Options

| Option                         | Type   | Default | Description                      |
| ------------------------------ | ------ | ------- | -------------------------------- |
| `server.memorySize`            | number | `1536`  | Server Lambda memory in MB       |
| `server.timeout`               | number | `30`    | Server Lambda timeout in seconds |
| `server.ephemeralStorageSize`  | number | `512`   | Ephemeral storage in MB          |
| `imageOptimisation.memorySize` | number | `1536`  | Image Lambda memory in MB        |
| `revalidation.memorySize`      | number | `768`   | Revalidation Lambda memory in MB |

### Messaging Options

| Option                                               | Type   | Default  | Description                       |
| ---------------------------------------------------- | ------ | -------- | --------------------------------- |
| `messaging.revalidationQueue.visibilityTimeout`      | number | `30`     | SQS visibility timeout in seconds |
| `messaging.revalidationQueue.messageRetentionPeriod` | number | `345600` | Message retention in seconds      |

***

## Breaking Out Components

Need more control? You can break out of the pattern and use individual factories.

### Why Break Out?

* Add additional resources (Redis, queues, etc.)
* Customise networking (VPC peering, private subnets)
* Share resources between multiple apps
* Fine-grained IAM permissions

### From Pattern to Factories

The Payload pattern can be approximated with this factory composition:

<Note>
  This example demonstrates factory composition concepts. The actual Payload pattern includes additional configuration like environment variables, IAM policies, and CDN behaviours that are automatically configured for optimal OpenNext deployment.
</Note>

```typescript theme={null}
import {
  App,
  DatabaseFactory,
  StorageFactory,
  ComputeFactory,
  CdnFactory,
  MessagingFactory,
} from "@fjall/components-infrastructure";
import * as lambda from "aws-cdk-lib/aws-lambda";

const app = App.getApp("MyPayloadApp");

// 1. Database (Aurora PostgreSQL)
const database = app.addDatabase(
  DatabaseFactory.build("PayloadDB", {
    type: "Aurora",
    databaseName: "payload",
  }),
);

// 2. Assets Storage (S3) - Static assets like JS, CSS, images
const assetsBucket = app.addStorage(StorageFactory.build("AssetsBucket", {}));

// 3. Media Storage (S3) - User uploads
const mediaBucket = app.addStorage(StorageFactory.build("MediaBucket", {}));

// 4. Cache Storage (S3) - ISR cache
const cacheBucket = app.addStorage(StorageFactory.build("CacheBucket", {}));

// 5. Tag Cache (DynamoDB)
const tagCache = app.addDatabase(
  DatabaseFactory.build("TagCache", {
    type: "DynamoDB",
    partitionKey: { name: "tag", type: "S" },
    sortKey: { name: "path", type: "S" },
    globalSecondaryIndexes: [
      {
        indexName: "revalidate",
        partitionKey: { name: "path", type: "S" },
        sortKey: { name: "revalidatedAt", type: "N" },
      },
    ],
  }),
);

// 6. Revalidation Queue (SQS FIFO)
const revalidationQueue = app.addMessaging(
  MessagingFactory.build("RevalidationQueue", {
    type: "queue",
    queueType: "fifo",
    deadLetterQueue: { enabled: true, maxReceiveCount: 3 },
  }),
);

// 7. Server Lambda (OpenNext)
// The actual pattern uses code deployment; container deployment shown for advanced use
const serverFunction = app.addCompute(
  ComputeFactory.build("Server", {
    type: "lambda",
    deployment: "code",
    handler: "index.handler",
    code: lambda.Code.fromAsset(".open-next/server-function"),
    memorySize: 1536,
    timeout: 30,
    connections: [
      database,
      assetsBucket,
      mediaBucket,
      cacheBucket,
      tagCache,
      revalidationQueue,
    ],
  }),
);

// 8. Image Optimisation Lambda
const imageFunction = app.addCompute(
  ComputeFactory.build("ImageOptimiser", {
    type: "lambda",
    deployment: "code",
    handler: "index.handler",
    code: lambda.Code.fromAsset(".open-next/image-optimization-function"),
    memorySize: 1536,
    connections: [assetsBucket],
  }),
);

// 9. Revalidation Lambda (processes ISR queue)
const revalidationFunction = app.addCompute(
  ComputeFactory.build("Revalidation", {
    type: "lambda",
    deployment: "code",
    handler: "index.handler",
    code: lambda.Code.fromAsset(".open-next/revalidation-function"),
    memorySize: 768,
    timeout: 30,
    connections: [revalidationQueue, tagCache, cacheBucket],
  }),
);

// 10. CloudFront CDN
// Note: CdnFactory uses originType to determine the origin source
const cdn = app.addCdn(
  CdnFactory.build("CDN", {
    originType: "auto",
    origin: serverFunction,
    behaviours: [
      {
        pathPattern: "/_next/image*",
        origin: imageFunction,
        cachePolicy: "CACHING_OPTIMIZED",
      },
      {
        pathPattern: "/_next/static/*",
        origin: assetsBucket,
        cachePolicy: "CACHING_OPTIMIZED",
      },
      {
        pathPattern: "/media/*",
        origin: mediaBucket,
        cachePolicy: "CACHING_OPTIMIZED",
      },
      {
        pathPattern: "/_next/data/*",
        origin: serverFunction,
        cachePolicy: "CACHING_DISABLED",
      },
    ],
  }),
);

export const infrastructure = app;
```

### Adding Extra Resources

Once broken out, easily add more resources:

```typescript theme={null}
// Add a separate worker queue
const workerQueue = app.addMessaging(
  MessagingFactory.build("WorkerQueue", {
    type: "queue",
    queueType: "standard",
  }),
);

// Add a worker Lambda
const worker = app.addCompute(
  ComputeFactory.build("Worker", {
    type: "lambda",
    deployment: "code",
    handler: "worker.handler",
    connections: [database, workerQueue],
  }),
);
```

### Sharing Resources Between Apps

Create shared infrastructure:

```typescript theme={null}
// shared/infrastructure.ts
import { App, DatabaseFactory } from "@fjall/components-infrastructure";

const shared = App.getApp("Shared");

export const sharedDatabase = shared.addDatabase(
  DatabaseFactory.build("SharedDB", {
    type: "Aurora",
    databaseName: "shared",
  }),
);

export const infrastructure = shared;
```

Reference from your Payload app:

```typescript theme={null}
// app/infrastructure.ts
import { App, ComputeFactory } from "@fjall/components-infrastructure";
import { sharedDatabase } from "../shared/infrastructure";

const app = App.getApp("PayloadApp");

const server = app.addCompute(
  ComputeFactory.build("Server", {
    type: "lambda",
    deployment: "container",
    connections: [sharedDatabase], // Reference shared resource
  }),
);

export const infrastructure = app;
```

***

## Troubleshooting

### Build Fails: "Error occurred prerendering page"

**Cause**: Page tries to access database at build time.

**Fix**: The Fjall CLI handles this automatically. Update to the latest version:

```bash theme={null}
npm update -g @fjall/cli
fjall deploy app
```

### 403 on Admin Panel Actions

**Cause**: CSRF configuration issue.

**Fix**: Set `csrf: []` explicitly in `payload.config.ts`:

```typescript theme={null}
export default buildConfig({
  cors: ["*"],
  csrf: [], // Must be explicit, not omitted
  // ...
});
```

### Media Uploads Disappear

**Cause**: Using local filesystem instead of S3.

**Fix**: Fjall automatically configures S3 storage. If you have customised `plugins/index.ts`, keep the S3 plugin in the list.

### "relation does not exist"

**Cause**: Migrations not generated or `prodMigrations` not configured.

**Fix**:

```bash theme={null}
npx payload migrate:create
git add src/migrations/
git commit -m "Add migrations"
fjall deploy app
```

***

## Costs

Estimated monthly costs for a low-traffic site:

| Resource       | Cost                                |
| -------------- | ----------------------------------- |
| RDS PostgreSQL | \~\$12-25 (db.t4g.micro, always on) |
| Lambda         | \~\$0-5 (pay per request)           |
| S3             | \~\$0.50-2                          |
| CloudFront     | \~\$1-5                             |
| **Total**      | **\~\$15-40/month**                 |

<Tip>
  Set `database.type: "Aurora"` to use Aurora Serverless v2, which scales to
  zero when idle and can lower the database cost for bursty or low-traffic sites.
</Tip>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Add Resources" icon="plus" href="/deployment/add-resources">
    Extend your app with additional AWS services
  </Card>

  <Card title="Compute Factory" icon="microchip" href="/patterns/compute-factory">
    Learn about Lambda and ECS options
  </Card>

  <Card title="Storage Factory" icon="database" href="/patterns/storage-factory">
    Configure databases and S3 buckets
  </Card>

  <Card title="CI/CD" icon="rotate" href="/patterns/buildkite-stack">
    Automate deployments with Buildkite
  </Card>
</CardGroup>

***

## Related Documentation

**Fjall:**

* [Deploy Application](/deployment/deploy-application) - General deployment guide
* [Compute Factory](/patterns/compute-factory) - Lambda and ECS configuration
* [Storage Factory](/patterns/storage-factory) - Database and S3 options
* [Network Factory](/patterns/network-factory) - VPC and networking

**External:**

* [Payload CMS Docs](https://payloadcms.com/docs) - Official Payload documentation
* [OpenNext](https://opennext.js.org/) - Next.js to AWS Lambda
* [Aurora Serverless v2](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.html) - AWS Aurora documentation
