--- tags: [payloadcms, tech-patterns] topic: payloadcms sources: [upload__overview.md, upload__storage-adapters.md] created: 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: ```ts 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): ```ts 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 ```ts beforeOperation: [ ({ req, operation }) => { if ((operation === 'create' || operation === 'update') && req.file) { req.file.name = 'custom-name.jpg' } }, ], ``` Custom per-size filename: ```ts { name: 'thumbnail', width: 400, height: 300, generateImageName: ({ height, sizeName, extension, width }) => `custom-${sizeName}-${height}-${width}.${extension}`, } ``` ## Code Examples ### Upload via REST (browser) ```ts 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) ```ts await payload.create({ collection: 'media', data: { alt: 'Seeded image' }, filePath: path.resolve(__dirname, 'seed/image.jpg'), }) ``` ### Restrict paste-URL to trusted domains ```ts upload: { pasteURL: { allowList: [ { hostname: 'assets.example.com', protocol: 'https', pathname: '/images/*' }, ], }, } ``` ### Custom admin thumbnail via function ```ts 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): ```ts 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): ```ts 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) ```ts 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 `` from `@payloadcms/ui` — a raw `` won't connect to the form state and causes 400 errors ## Related - [[wiki/payloadcms/configuration|Configuration]] - [[wiki/payloadcms/fields-complex|Fields Complex]] - [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]]