ChaiBuilder Logo

Storage Configuration

For uploaded images and files to persist in production, the media collection must write to an object store rather than the local filesystem. ChaiBuilder uses Payload's official S3 storage plugin (@payloadcms/storage-s3), which works with any S3-compatible backend - AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Wasabi, and more.

Why this is required: on serverless and containerised hosts the local filesystem is ephemeral - files written at runtime disappear on redeploy and aren't shared across instances. Object storage keeps uploads durable and centralized. See Overview.

1. Provision a bucket

Create a bucket on your provider and an access key pair scoped to it. Note down:

  • Bucket name
  • Region
  • Access key ID and secret access key
  • Endpoint - the S3 API endpoint. For AWS this is inferred from the region; for S3-compatible providers (R2, MinIO, Spaces) it's an explicit URL.

2. Set environment variables

Add your credentials to the project's environment. ChaiBuilder's Payload config reads these:

# --- S3-compatible object storage (media uploads) ---
BUCKET_NAME=your-bucket
AWS_ACCESS_KEY_ID=your_access_key_id
AWS_SECRET_ACCESS_KEY=your_secret_access_key
S3_REGION=auto                                           # e.g. us-east-1, or "auto" for Cloudflare R2
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com   # provider S3 API endpoint

BUCKET_NAME, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY are the three that switch storage on. S3_REGION defaults to auto when unset, and S3_ENDPOINT is optional - leave it out on AWS S3, set it for R2, MinIO, and Spaces.

The plugin is only registered when all three required variables and CHAIBUILDER_APP_KEY are present, since the storage prefix is derived from the app key. Miss any one of them and the app quietly stays on local disk.

The S3 plugin block in payload.config.ts and your project's .env.example are the source of truth for these names.

3. Wire the plugin (already in the starter)

The ChaiBuilder starter already registers the S3 plugin in payload.config.ts, pointing the media collection at your bucket. It looks like this:

import { s3Storage } from '@payloadcms/storage-s3'

export default buildConfig({
  // …
  plugins: [
    // …
    s3Storage({
      collections: {
        media: { prefix: getAppStoragePrefix() },  // keeps each app's files namespaced
      },
      bucket: process.env.BUCKET_NAME!,
      config: {
        credentials: {
          accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
          secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
        },
        region: process.env.S3_REGION || 'auto',
        endpoint: process.env.S3_ENDPOINT || undefined,
      },
    }),
  ],
})

Notes:

  • collections.media attaches the adapter to the media upload collection. Add other upload collections here the same way if you create them.
  • prefix namespaces objects per app, so a shared bucket won't collide across projects.
  • region falls back to auto, which is what Cloudflare R2 expects. Set S3_REGION to a real region on AWS S3 and other providers.
  • endpoint is left undefined when S3_ENDPOINT is unset, so the AWS SDK infers the AWS S3 endpoint from the region.
  • The starter does not set forcePathStyle. R2, MinIO, and Spaces work with the default virtual-hosted addressing. If your provider needs path-style URLs, add forcePathStyle: true to the config block yourself.

4. Restart and verify

Restart the app, open the media manager in the builder, and upload an image. Then:

  • Confirm the image renders in the builder and on a published page.
  • Check your bucket - the object should appear under the configured prefix.
  • Redeploy and confirm existing images still load (the point of object storage).

Provider quick reference

Provider S3_ENDPOINT S3_REGION Notes
AWS S3 omit (inferred from region) e.g. us-east-1 must set a real region
Cloudflare R2 https://<account>.r2.cloudflarestorage.com auto (the default)
MinIO your MinIO URL any non-empty value may need forcePathStyle: true in config
DigitalOcean Spaces https://<region>.digitaloceanspaces.com region slug

Troubleshooting

  • Uploads succeed but images 404 after deploy - you're still on the local filesystem; the S3 env vars aren't set or aren't reaching the runtime.
  • AccessDenied / signature errors - key pair lacks bucket permissions, wrong region, or wrong endpoint.
  • Works locally, fails on the host - env vars not configured in the deployment platform's settings (they aren't read from a local .env in production).

© ChaiBuilder. All rights reserved.