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
1pnpm add @payloadcms/storage-s3 @aws-sdk/client-s3

2. Configure R2 in Cloudflare

  1. Navigate to Cloudflare Dashboard > R2
  2. Create a new bucket:
    • Click "Create bucket"
    • Enter a unique bucket name
    • Choose your preferred region
  3. Generate API credentials:
    • Go to "R2 > Manage R2 API Tokens"
    • Click "Create API Token"
    • Select permissions:
      • "Object Read" for read-only access
      • "Object Write" for upload capabilities
      • Both for full access
    • 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_ACCOUNT_ID=your_account_id
6R2_ENDPOINT=https://${R2_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";
3import path from "path";
4
5const storage = s3Storage({
6 collections: {
7 media: {
8 adapter: "s3", // Required
9 disableLocalStorage: true, // Recommended for production
10 prefix: "media", // Optional prefix for uploaded files
11 generateFileURL: ({ filename, prefix }) =>
12 `https://<your-bucket>.<your-domain>/${prefix}/${filename}`, // Optional
13 },
14 },
15 bucket: process.env.R2_BUCKET,
16 config: {
17 endpoint: process.env.R2_ENDPOINT,
18 credentials: {
19 accessKeyId: process.env.R2_ACCESS_KEY_ID,
20 secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
21 },
22 region: "auto", // Required for R2
23 forcePathStyle: true, // Required for R2
24 },
25});
26
27export default buildConfig({
28 admin: {
29 // Your admin config
30 },
31 collections: [
32 {
33 slug: "media",
34 upload: {
35 staticDir: path.resolve(__dirname, "../media"),
36 // Image sizes are processed locally before upload
37 imageSizes: [
38 {
39 name: "thumbnail",
40 width: 400,
41 height: 300,
42 position: "centre",
43 },
44 {
45 name: "card",
46 width: 768,
47 height: 1024,
48 position: "centre",
49 },
50 ],
51 formatOptions: {
52 format: "webp", // Convert uploads to WebP
53 options: {
54 quality: 80,
55 },
56 },
57 },
58 fields: [], // Add custom fields as needed
59 },
60 ],
61 plugins: [storage],
62});

5. Public Access Configuration

To make your R2 bucket publicly accessible, you have two options:

  1. Using Cloudflare Workers (Recommended):
typescript
1// worker.js
2export default {
3 async fetch(request, env) {
4 const url = new URL(request.url);
5 const objectKey = url.pathname.slice(1); // Remove leading slash
6
7 try {
8 const object = await env.MY_BUCKET.get(objectKey);
9
10 if (!object) {
11 return new Response("Object Not Found", { status: 404 });
12 }
13
14 const headers = new Headers();
15 object.writeHttpMetadata(headers);
16 headers.set("etag", object.httpEtag);
17
18 return new Response(object.body, {
19 headers,
20 });
21 } catch (error) {
22 return new Response("Internal Error", { status: 500 });
23 }
24 },
25};
  1. Using Public Bucket (Simpler but less control):
    • Enable public access in R2 bucket settings
    • Update your generateFileURL config to use the public bucket URL

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