
Made with ChaiBuilder
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.
Create a bucket on your provider and an access key pair scoped to it. Note down:
Add your credentials to the project's environment. ChaiBuilder's Payload config reads these:
# --- S3-compatible object storage (media uploads) ---
BUCKET_NAME=your-bucket
REGION=auto # e.g. us-east-1, or "auto" for Cloudflare R2
ACCESS_KEY_ID=your_access_key_id
SECRET_ACCESS_KEY=your_secret_access_key
ENDPOINT=https://<account>.r2.cloudflarestorage.com # provider S3 API endpoint
Confirm the exact variable names against your generated project's
.env.example— the S3 plugin block inpayload.config.tsis what maps them, so those two files are the source of truth for your 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 as string,
config: {
credentials: {
accessKeyId: process.env.ACCESS_KEY_ID as string,
secretAccessKey: process.env.SECRET_ACCESS_KEY as string,
},
region: process.env.REGION as string,
endpoint: process.env.ENDPOINT as string,
forcePathStyle: true, // required by most S3-compatible providers (R2, MinIO, …)
},
}),
],
})
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.forcePathStyle: true is needed by R2, MinIO, and most non-AWS S3 APIs. On AWS S3 you
can drop it and omit endpoint.Restart the app, open the media manager in the builder, and upload an image. Then:
| Provider | ENDPOINT |
REGION |
Notes |
|---|---|---|---|
| AWS S3 | omit (inferred) | e.g. us-east-1 |
forcePathStyle not required |
| Cloudflare R2 | https://<account>.r2.cloudflarestorage.com |
auto |
forcePathStyle: true |
| MinIO | your MinIO URL | any | forcePathStyle: true |
| DigitalOcean Spaces | https://<region>.digitaloceanspaces.com |
region slug | forcePathStyle: true |
AccessDenied / signature errors — key pair lacks bucket permissions, wrong region, or
wrong endpoint..env in production).