Bridger Logo

Bridger Tower / Designer and Software Engineer

Use Cloudflare R2 with Payload CMS

A guide to integrating Cloudflare R2 with Payload CMS for efficient media storage

This guide demonstrates how to integrate Cloudflare R2 with Payload CMS for efficient media storage. Cloudflare R2 offers an S3-compatible storage solution with significant cost advantages, particularly its zero egress fees policy.

Why Choose R2 with Payload CMS?

  • Cost Efficiency: Zero egress fees, unlike traditional S3 providers
  • Global Performance: Leverage Cloudflare's global edge network
  • Native Integration: Full S3 compatibility with Payload CMS
  • Simplified Management: Easy-to-use dashboard and API
  • Predictable Pricing: Pay only for storage used, not for bandwidth

Prerequisites

  • Cloudflare account with R2 access enabled
  • Payload CMS project (version 3.x or later)
  • Node.js 16.x or later
  • Package manager (npm, yarn, or pnpm)

Implementation Guide

1. Install Required Dependencies

bash
1# Using npm
2npm install @payloadcms/storage-s3
3
4# Using yarn
5yarn add @payloadcms/storage-s3
6
7# Using pnpm
8pnpm add @payloadcms/storage-s3

2. Configure R2 in Cloudflare

  1. Navigate to Cloudflare Dashboard
  2. Select "R2" from the sidebar
  3. Create a new bucket:
    • Click "Create bucket"
    • Enter a unique bucket name
    • Choose your preferred region
  4. Generate API credentials:
    • Go to "R2 > Manage R2 API Tokens"
    • Click "Create API Token"
    • Select "Admin Read & Write" permissions
    • Save your credentials securely

3. Set Up Environment Variables

Create or update your .env file:

bash
1# .env
2R2_ACCESS_KEY_ID=your_access_key_id
3R2_SECRET_ACCESS_KEY=your_secret_access_key
4R2_BUCKET=your_bucket_name
5R2_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com

4. Configure Payload CMS

Update your payload.config.ts:

typescript
1import { buildConfig } from "payload/config";
2import { s3Storage } from "@payloadcms/storage-s3";
3
4export default buildConfig({
5 plugins: [
6 s3Storage({
7 collections: {
8 media: {
9 adapter: "s3",
10 disableLocalStorage: true, // Optional: disable local storage
11 prefix: "media", // Optional: prefix all files with this path
12 },
13 },
14 bucket: process.env.R2_BUCKET!,
15 config: {
16 endpoint: process.env.R2_ENDPOINT!,
17 credentials: {
18 accessKeyId: process.env.R2_ACCESS_KEY_ID!,
19 secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
20 },
21 region: "auto",
22 forcePathStyle: true,
23 },
24 }),
25 ],
26});

5. Media Collection Configuration

Configure your media collection to work with R2:

typescript
1import { CollectionConfig } from "payload/types";
2
3export const Media: CollectionConfig = {
4 slug: "media",
5 upload: {
6 staticURL: "/media",
7 staticDir: "media",
8 mimeTypes: ["image/*", "application/pdf"], // Customize as needed
9 imageSizes: [
10 {
11 name: "thumbnail",
12 width: 400,
13 height: 300,
14 position: "centre",
15 },
16 {
17 name: "card",
18 width: 768,
19 height: 1024,
20 position: "centre",
21 },
22 ],
23 },
24 fields: [], // Add custom fields as needed
25};

Advanced Configuration

Custom Upload Handlers

typescript
1s3Storage({
2 // ...other config
3 beforeUpload: async ({ req, file }) => {
4 // Customize file before upload
5 return file;
6 },
7 afterUpload: async ({ req, file, collection }) => {
8 // Perform actions after successful upload
9 console.log(`File ${file.filename} uploaded to ${collection.slug}`);
10 },
11});

Error Handling

Implement robust error handling:

typescript
1try {
2 await payload.create({
3 collection: "media",
4 data: {
5 // your upload data
6 },
7 });
8} catch (error) {
9 if (error.code === "AccessDenied") {
10 console.error("R2 access denied - check credentials");
11 } else if (error.code === "NoSuchBucket") {
12 console.error("R2 bucket not found");
13 } else {
14 console.error("Upload failed:", error);
15 }
16}

Troubleshooting Guide

Common Issues

Connection Errors

  • Verify R2 credentials are correct
  • Confirm endpoint URL format
  • Check network connectivity
  • Validate IP allowlist settings

Upload Failures

  • Verify bucket exists and is accessible
  • Check API token permissions
  • Confirm file size limits
  • Validate MIME type restrictions

URL Generation Issues

  • Verify public bucket configuration
  • Check custom domain settings
  • Validate URL formatting

Health Check

Run this diagnostic code to verify your setup:

typescript
1async function checkR2Setup() {
2 try {
3 const testUpload = await payload.create({
4 collection: "media",
5 data: {
6 // test file data
7 },
8 });
9 console.log("R2 connection successful:", testUpload);
10 } catch (error) {
11 console.error("R2 setup check failed:", error);
12 }
13}

Performance Optimization

File Compression

  • Implement image compression before upload
  • Use appropriate file formats
  • Set reasonable size limits

Caching Strategy

  • Configure browser caching headers
  • Implement cache-control policies
  • Use Cloudflare's caching features

Security Best Practices

Access Control

  • Use least-privilege access tokens
  • Implement bucket policies
  • Enable audit logging

Data Protection

  • Enable encryption at rest
  • Implement secure file validation
  • Regular security audits

Resources

  • Payload CMS Documentation
  • Cloudflare R2 Documentation
  • S3 Plugin Documentation
  • Cloudflare Workers Documentation

Support

For additional help:

  • Join the Payload CMS Discord
  • Visit the Cloudflare Community Forums
  • Submit issues on GitHub

© Bridger Tower, 2025