#r2 with s3storage generates file paths in database and bucket, but payload uses server path

7 messages · Page 1 of 1 (latest)

silent silo
#

Exactly as mentioned. There's an old thread seemingly related, but was too generic. I do have Folders. The image paths do update and exist in CF after resizing. Only the uploaded asset entry is added to the database.

Payload Admin seems to only reference the local API file path and not CF.

PayloadCMS 3.81, Next.js 15.4.11.
Railway (hobby/paid), MongoDB Atlas M0, Cloudflare R2 bucket.

Followed the docs here: https://payloadcms.com/docs/upload/storage-adapters#s3-r2

Any ideas?

loud jungleBOT
broken badger
#

Hey @silent silo so you're looking in the right spot!

This is likely the issue:

R2 buckets are private by default. The S3 API endpoint (e.g. https://<accountId>.r2.cloudflarestorage.com) is used for uploading files only — it cannot serve files publicly. To serve files, enable the R2.dev subdomain or connect a custom domain to your bucket in the Cloudflare dashboard, then pass that public URL to generateFileURL.

#
collections: {
  media: {
    generateFileURL: ({ filename, prefix }) => {
      const key = [prefix, filename].filter(Boolean).join('/')
      return `${process.env.R2_PUBLIC_URL}/${key}`
    },
  },
},
silent silo
# broken badger Hey <@1461752478008737863> so you're looking in the right spot! This is likely ...

Thanks @broken badger - I was able to get it working with brute force choices.

Media.ts has disableLocalStorage: true

s3Storage plugin is configured like tihs:

  enabled: Boolean(process.env.R2_BUCKET),
    disableLocalStorage: true,
    collections: {
      media: {
        generateFileURL: ({ filename, prefix }) => {
          const key = [prefix, filename].filter(Boolean).join('/')
          return `${process.env.R2_PUBLIC_URL}/${key}`
        },
      },
    },

I ended up adding this to my next.config.js as well. I let Claude take the wheel and it led to this:

// Build remote patterns for Next.js Image optimization
const remotePatterns = []

// Add R2/Cloudflare URL if configured
const r2PublicUrl = process.env.R2_PUBLIC_URL
if (r2PublicUrl) {
  try {
    const url = new URL(r2PublicUrl)
    remotePatterns.push({
      hostname: url.hostname,
      protocol: url.protocol.replace(':', ''),
    })
  } catch (e) {
    console.warn('Invalid R2_PUBLIC_URL:', r2PublicUrl)
  }
}

// Add localhost for development (when no R2 configured)
if (!r2PublicUrl) {
  remotePatterns.push({
    hostname: 'localhost',
    protocol: 'http',
  })
}

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns,
  },
  webpack: (webpackConfig) => {
    webpackConfig.resolve.extensionAlias = {
      '.cjs': ['.cts', '.cjs'],
      '.js': ['.ts', '.tsx', '.js', '.jsx'],
      '.mjs': ['.mts', '.mjs'],
    }

    return webpackConfig
  },
  reactStrictMode: true,
  redirects,
}
loud jungleBOT
broken badger
#

Glad you got it working!