Use Cloudflare R2 with Payload CMS
Written by Bridger Tower
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
# Using npm npm install @payloadcms/storage-s3 # Using yarn yarn add @payloadcms/storage-s3 # Using pnpm pnpm add @payloadcms/storage-s3
2. Configure R2 in Cloudflare
- Navigate to Cloudflare Dashboard
 - Select "R2" from the sidebar
 - Create a new bucket:
 - Click "Create bucket"
 - Enter a unique bucket name
 - Choose your preferred region
 
- 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
# .env R2_ACCESS_KEY_ID=your_access_key_id R2_SECRET_ACCESS_KEY=your_secret_access_key R2_BUCKET=your_bucket_name R2_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com
4. Configure Payload CMS
Update your payload.config.ts:
typescript
import { buildConfig } from 'payload/config'
import { s3Storage } from '@payloadcms/storage-s3'
export default buildConfig({
  plugins: [
    s3Storage({
      collections: {
        media: {
          adapter: 's3',
          disableLocalStorage: true, // Optional: disable local storage
          prefix: 'media', // Optional: prefix all files with this path
        },
      },
      bucket: process.env.R2_BUCKET!,
      config: {
        endpoint: process.env.R2_ENDPOINT!,
        credentials: {
          accessKeyId: process.env.R2_ACCESS_KEY_ID!,
          secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
        },
        region: 'auto',
        forcePathStyle: true,
      },
    }),
  ],
})5. Media Collection Configuration
Configure your media collection to work with R2:
typescript
import { CollectionConfig } from 'payload/types'
export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    staticURL: '/media',
    staticDir: 'media',
    mimeTypes: ['image/*', 'application/pdf'], // Customize as needed
    imageSizes: [
      {
        name: 'thumbnail',
        width: 400,
        height: 300,
        position: 'centre',
      },
      {
        name: 'card',
        width: 768,
        height: 1024,
        position: 'centre',
      },
    ],
  },
  fields: [], // Add custom fields as needed
}Advanced Configuration
Custom Upload Handlers
typescript
s3Storage({
  // ...other config
  beforeUpload: async ({ req, file }) => {
    // Customize file before upload
    return file
  },
  afterUpload: async ({ req, file, collection }) => {
    // Perform actions after successful upload
    console.log(`File ${file.filename} uploaded to ${collection.slug}`)
  },
})Error Handling
Implement robust error handling:
typescript
try {
  await payload.create({
    collection: 'media',
    data: {
      // your upload data
    },
  })
} catch (error) {
  if (error.code === 'AccessDenied') {
    console.error('R2 access denied - check credentials')
  } else if (error.code === 'NoSuchBucket') {
    console.error('R2 bucket not found')
  } else {
    console.error('Upload failed:', error)
  }
}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
async function checkR2Setup() {
  try {
    const testUpload = await payload.create({
      collection: 'media',
      data: {
        // test file data
      },
    })
    console.log('R2 connection successful:', testUpload)
  } catch (error) {
    console.error('R2 setup check failed:', error)
  }
}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