Payload CMS Media Library
Payload manages media files through a dedicated collection with the upload type. Files can be stored locally or in the cloud (S3, Cloudflare R2, GCS) through official adapter plugins.
Basic Media Collection
// collections/Media.ts
import type { CollectionConfig } from 'payload/types'
const Media: CollectionConfig = {
slug: 'media',
upload: {
staticURL: '/media',
staticDir: 'public/media', // local storage
imageSizes: [
{ name: 'thumbnail', width: 400, height: 300, crop: 'center' },
{ name: 'card', width: 768, height: 512, crop: 'center' },
{ name: 'tablet', width: 1024, withoutEnlargement: true },
],
adminThumbnail: 'thumbnail',
mimeTypes: ['image/*', 'application/pdf'],
// maximum file size (bytes)
limits: { fileSize: 10 * 1024 * 1024 },
},
fields: [
{
name: 'alt',
type: 'text',
required: true,
},
{
name: 'caption',
type: 'text',
},
],
}
export default Media
S3 Storage via Plugin
npm install @payloadcms/storage-s3
// payload.config.ts
import { s3Storage } from '@payloadcms/storage-s3'
export default buildConfig({
plugins: [
s3Storage({
collections: {
media: {
prefix: 'media',
generateFileURL: ({ filename, prefix }) =>
`${process.env.CDN_URL}/${prefix}/${filename}`,
},
},
bucket: process.env.S3_BUCKET!,
config: {
region: process.env.S3_REGION || 'eu-west-1',
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY!,
secretAccessKey: process.env.S3_SECRET_KEY!,
},
endpoint: process.env.S3_ENDPOINT, // for Cloudflare R2 or MinIO
},
}),
],
})
For Cloudflare R2, set endpoint = https://<account-id>.r2.cloudflarestorage.com and region = auto.
Cloudflare R2 + Image Transforms
// collections/Media.ts — URL transformation via Cloudflare Images
upload: {
imageSizes: [
{
name: 'thumbnail',
generateImageName: ({ height, sizeName, extension, width }) =>
`${sizeName}_${width}x${height}.${extension}`,
},
],
// For R2 — don't generate server-side resizes, use CF Image Resizing
disableLocalStorage: true,
}
File Upload via API
// Upload via REST API
const formData = new FormData()
formData.append('file', fileBlob, 'image.jpg')
formData.append('alt', 'Image description')
const response = await fetch('/api/media', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
})
const media = await response.json()
// media.url — file URL
// media.sizes.thumbnail.url — resized URL
Usage in Other Collections
// Upload field in collection
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
required: true,
}
// In Next.js component
import Image from 'next/image'
const PostCard = ({ post }: { post: Post }) => {
const image = post.featuredImage
if (typeof image === 'string') return null // not populated
return (
<Image
src={image.sizes?.card?.url || image.url!}
alt={image.alt}
width={768}
height={512}
/>
)
}
Timeline
Setting up a media library with S3/R2 storage, resizing, and collection integration takes 0.5–1 day.







