obsidian/wiki/payloadcms/upload.md
2026-05-15 15:13:56 +01:00

7.3 KiB

tags topic sources created
payloadcms
tech-patterns
payloadcms
upload__overview.md
upload__storage-adapters.md
2026-05-15

PayloadCMS — Upload & Media

Overview

Enabling upload on a collection transforms it into a full file management system. Payload auto-injects filename, mimeType, filesize (and optionally sizes) fields and modifies the Admin Panel list/edit views to support file operations.

Common use cases:

  • Media library for site images
  • Gated content (PDFs, ebooks behind auth)
  • Publicly downloadable assets (ZIPs, MP4s)

Upload only works via REST and Local APIs — not GraphQL.

Setup / Config

Minimal collection setup:

import type { CollectionConfig } from 'payload'

export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    staticDir: 'media',        // relative to config file
    mimeTypes: ['image/*'],
    adminThumbnail: 'thumbnail',
    imageSizes: [
      { name: 'thumbnail', width: 400, height: 300, position: 'centre' },
      { name: 'card', width: 768, height: 1024, position: 'centre' },
      { name: 'tablet', width: 1024, height: undefined, position: 'centre' },
    ],
  },
  fields: [{ name: 'alt', type: 'text' }],
}

Global file size limit (Busboy options):

export default buildConfig({
  upload: {
    limits: { fileSize: 5_000_000 }, // 5 MB in bytes
  },
})

Key Options

Collection-level upload config

Option Notes
staticDir Local storage folder; defaults to collection slug
imageSizes Array of resize targets (uses sharp)
adminThumbnail String (size name) or ({ doc }) => url function
mimeTypes Array — restricts file picker: ['image/*', 'application/pdf']
disableLocalStorage Set true when using a cloud storage adapter
crop / focalPoint Both true by default; set false to disable UI tools
resizeOptions Single sharp resize (alternative to imageSizes)
formatOptions Sharp output format override
pasteURL false to disable URL paste; object with allowList for server-side CORS fetch
bulkUpload true by default
filesRequiredOnCreate true by default
withMetadata Append EXIF metadata to output image
modifyResponseHeaders Manipulate HTTP response headers for served files
allowRestrictedFileTypes true to bypass blocked executable/script extension list

Image sizes — withoutEnlargement

  • undefined (default) — returns null when upload is smaller than defined size
  • false — always enlarge
  • true — return original image when smaller

Custom filename via beforeOperation hook

beforeOperation: [
  ({ req, operation }) => {
    if ((operation === 'create' || operation === 'update') && req.file) {
      req.file.name = 'custom-name.jpg'
    }
  },
],

Custom per-size filename:

{
  name: 'thumbnail',
  width: 400, height: 300,
  generateImageName: ({ height, sizeName, extension, width }) =>
    `custom-${sizeName}-${height}-${width}.${extension}`,
}

Code Examples

Upload via REST (browser)

const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('_payload', JSON.stringify({ alt: 'My image' }))
// Do NOT set Content-Type manually — browser sets multipart boundary
fetch('/api/media', { method: 'POST', body: formData })

Upload from local path (seed scripts)

await payload.create({
  collection: 'media',
  data: { alt: 'Seeded image' },
  filePath: path.resolve(__dirname, 'seed/image.jpg'),
})

Restrict paste-URL to trusted domains

upload: {
  pasteURL: {
    allowList: [
      { hostname: 'assets.example.com', protocol: 'https', pathname: '/images/*' },
    ],
  },
}

Custom admin thumbnail via function

upload: {
  adminThumbnail: ({ doc }) =>
    `https://cdn.example.com/${doc.filename}?w=200`,
}

Storage Adapters

All adapters auto-set disableLocalStorage: true per collection.

Service Package
Vercel Blob @payloadcms/storage-vercel-blob
AWS S3 @payloadcms/storage-s3
Azure Blob @payloadcms/storage-azure
GCS @payloadcms/storage-gcs
Uploadthing @payloadcms/storage-uploadthing
R2 (Workers) @payloadcms/storage-r2

S3 example (also works for Cloudflare R2 via S3 API):

import { s3Storage } from '@payloadcms/storage-s3'

plugins: [
  s3Storage({
    collections: { media: true },
    bucket: process.env.S3_BUCKET,
    config: {
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
      },
      region: process.env.S3_REGION,
    },
  }),
]

Cloudflare R2 via S3 API (Node.js / Vercel):

s3Storage({
  enabled: Boolean(process.env.R2_BUCKET),
  collections: {
    media: {
      disablePayloadAccessControl: true,
      generateFileURL: ({ filename, prefix }) => {
        const key = prefix ? `${prefix}/${filename}` : filename
        return `${process.env.R2_PUBLIC_URL}/${key}`
      },
    },
  },
  bucket: process.env.R2_BUCKET,
  config: {
    credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY },
    region: 'auto',           // required by R2
    endpoint: process.env.R2_ENDPOINT,
    forcePathStyle: true,     // required for R2
  },
})

Prefix composition

By default a document-level prefix overrides the collection prefix. Use useCompositePrefixes: true to combine them:

Collection prefix: uploads   +   Document prefix: user-123
Default  → user-123/file.jpg
Composite → uploads/user-123/file.jpg

Payload Access Control for cloud files

By default file URLs stay at /collectionSlug/staticURL/filename so access control still applies. Set disablePayloadAccessControl: true only when your collection uses read: () => true and you want direct CDN URLs.

Custom storage adapter (base plugin)

import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'

plugins: [
  cloudStoragePlugin({
    collections: {
      'my-collection': { adapter: myCustomAdapter },
    },
  }),
]

GeneratedAdapter interface requires: handleDelete, handleUpload, staticHandler, name.

Gotchas

  • sharp must be configured in Payload config for image resizing to work (auto-configured by create-payload-app)
  • withoutEnlargement defaults to undefined — images smaller than a defined size return null for that size, not the original
  • Image cropping happens before resizing — resized sizes are derived from the cropped image
  • Focal point selector only shows up when imageSizes or resizeOptions is defined
  • Vercel server uploads are limited to 4.5 MB — set clientUploads: true on the adapter to bypass
  • R2: the S3 API endpoint is for uploads only; serve files via R2_PUBLIC_URL (r2.dev subdomain or custom domain)
  • Restricted file types (.exe, .php, .js, HTML, scripts) are blocked by default unless mimeTypes is set or allowRestrictedFileTypes: true
  • Custom upload components must wrap Payload's <Upload> from @payloadcms/ui — a raw <input type="file"> won't connect to the form state and causes 400 errors