From 57d13f19c17a9da7eebb0f022ba8c14ee9c88539 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Fri, 15 May 2026 16:54:04 +0100 Subject: [PATCH] vault backup: 2026-05-15 16:54:04 --- raw/{ => _processed}/upload__overview.md | 0 wiki/payloadcms/storage-adapters.md | 233 +++++++++++++++++++++++ wiki/payloadcms/upload.md | 20 +- 3 files changed, 251 insertions(+), 2 deletions(-) rename raw/{ => _processed}/upload__overview.md (100%) create mode 100644 wiki/payloadcms/storage-adapters.md diff --git a/raw/upload__overview.md b/raw/_processed/upload__overview.md similarity index 100% rename from raw/upload__overview.md rename to raw/_processed/upload__overview.md diff --git a/wiki/payloadcms/storage-adapters.md b/wiki/payloadcms/storage-adapters.md new file mode 100644 index 0000000..e37b5f1 --- /dev/null +++ b/wiki/payloadcms/storage-adapters.md @@ -0,0 +1,233 @@ +--- +title: "Storage Adapters" +aliases: [payload-storage, cloud-storage, s3-storage, gcs-storage, azure-storage] +tags: [payloadcms, upload, storage, s3, gcs, azure, vercel, cloudflare, r2] +sources: [raw/upload__storage-adapters.md] +created: 2026-05-15 +updated: 2026-05-15 +--- + +# Storage Adapters + +Payload ships with local disk storage by default. For production, swap to a cloud adapter — all adapters automatically set `disableLocalStorage: true` on the configured collections. + +## Available Adapters + +| Service | Package | +|---------|---------| +| Vercel Blob | `@payloadcms/storage-vercel-blob` | +| AWS S3 | `@payloadcms/storage-s3` | +| Azure Blob | `@payloadcms/storage-azure` | +| Google Cloud Storage | `@payloadcms/storage-gcs` | +| Uploadthing | `@payloadcms/storage-uploadthing` | +| Cloudflare R2 (Workers) | `@payloadcms/storage-r2` | + +## Vercel Blob + +```ts +import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' + +vercelBlobStorage({ + enabled: true, + collections: { + media: true, + 'media-with-prefix': { prefix: 'my-prefix' }, + }, + token: process.env.BLOB_READ_WRITE_TOKEN, +}) +``` + +- Requires `BLOB_READ_WRITE_TOKEN` (auto-set by Vercel after adding blob storage) +- Vercel server upload limit: **4.5 MB** — use `clientUploads: true` to bypass + +| Option | Default | +|--------|---------| +| `enabled` | `true` | +| `addRandomSuffix` | `false` | +| `cacheControlMaxAge` | 1 year | +| `clientUploads` | — | +| `useCompositePrefixes` | `false` | + +## AWS S3 + +```ts +import { s3Storage } from '@payloadcms/storage-s3' + +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, + }, +}) +``` + +- `config` accepts any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) +- `signedDownloads` — use presigned URLs per-collection (useful for large files/videos) +- `enabled: Boolean(process.env.S3_BUCKET)` — skip plugin when credentials absent in local dev + +### Cloudflare R2 via S3 API + +Use `@payloadcms/storage-s3` for R2 from Node.js/Vercel/Netlify — **not** `@payloadcms/storage-r2` (that's Cloudflare Workers only). + +```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, // S3 API endpoint, uploads only + forcePathStyle: true, // required for R2 + }, +}) +``` + +Required env vars: +``` +R2_BUCKET=my-bucket +R2_ACCESS_KEY_ID=... +R2_SECRET_ACCESS_KEY=... +R2_ENDPOINT=https://.r2.cloudflarestorage.com +R2_PUBLIC_URL=https://media.yourdomain.com # R2.dev subdomain or custom domain +``` + +> **Warning:** R2 buckets are private by default. The S3 API endpoint is for uploads only — you must enable the R2.dev subdomain or connect a custom domain to serve files publicly. + +## Azure Blob Storage + +```ts +import { azureStorage } from '@payloadcms/storage-azure' + +azureStorage({ + collections: { media: true }, + allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true', + baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL, + connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, + containerName: process.env.AZURE_STORAGE_CONTAINER_NAME, +}) +``` + +## Google Cloud Storage + +```ts +import { gcsStorage } from '@payloadcms/storage-gcs' + +gcsStorage({ + collections: { media: true }, + bucket: process.env.GCS_BUCKET, + options: { + apiEndpoint: process.env.GCS_ENDPOINT, + projectId: process.env.GCS_PROJECT_ID, + }, +}) +``` + +## Uploadthing + +```ts +uploadthingStorage({ + collections: { media: true }, + options: { + token: process.env.UPLOADTHING_TOKEN, + acl: 'public-read', + }, +}) +``` + +## Cloudflare R2 (Workers native) + +```ts +r2Storage({ + collections: { media: true }, + bucket: cloudflare.env.R2, // native R2 bucket binding +}) +``` + +> Beta. Use this only in Cloudflare Workers where R2 is available as a native binding. For all other Node.js environments, use `@payloadcms/storage-s3` with the R2 S3-compatible endpoint. + +## Prefix Composition + +By default, a document-level prefix **overrides** the collection prefix entirely. + +With `useCompositePrefixes: true`, they are **combined**: + +``` +# Default +Collection prefix: media-folder, Document prefix: user-123 +→ user-123/image.jpg + +# useCompositePrefixes: true +→ media-folder/user-123/image.jpg +``` + +## Payload Access Control + +By default, Payload proxies all file requests through `///` so that access control rules apply even to cloud-hosted files. The file URL does **not** point directly to the cloud host. + +Set `disablePayloadAccessControl: true` on a collection to bypass the proxy and serve files directly from the cloud host. Only safe when `read: () => true` or similar. + +## Custom Adapter + +Use `@payloadcms/plugin-cloud-storage` as the base: + +```ts +import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage' + +cloudStoragePlugin({ + collections: { + 'my-collection-slug': { adapter: myCustomAdapter }, + }, +}) +``` + +Implement the `GeneratedAdapter` interface: + +```ts +interface GeneratedAdapter { + fields?: Field[] + generateURL?: GenerateURL + handleDelete: HandleDelete + handleUpload: HandleUpload + name: string + onInit?: () => void + staticHandler: StaticHandler +} +``` + +## Key Takeaways + +- All official adapters auto-set `disableLocalStorage: true` on their collections +- **Vercel 4.5 MB limit** — set `clientUploads: true` + allow CORS PUT on your bucket +- **R2 + Node.js** → use `storage-s3` with `region: 'auto'`, `forcePathStyle: true`, `endpoint` for uploads, separate `R2_PUBLIC_URL` for serving +- `useCompositePrefixes: true` combines collection + document prefixes instead of document overriding collection +- Access control proxying is on by default — disable only when collection is fully public +- Conditionally enable with `enabled: Boolean(process.env.BUCKET)` to skip in local dev + +## Related + +- [[wiki/payloadcms/upload|Upload & Media]] — upload collection config, imageSizes, focal point +- [[wiki/payloadcms/production-deployment|Production Deployment]] — ephemeral vs persistent file storage, Dockerfile +- [[wiki/payloadcms/access-control|Access Control]] — read access applied to static file serving +- [[wiki/payloadcms/plugin-form-builder|Form Builder Plugin]] — presigned URL pattern for form uploads + +## Sources + +- `raw/upload__storage-adapters.md` +- https://payloadcms.com/docs/upload/storage-adapters diff --git a/wiki/payloadcms/upload.md b/wiki/payloadcms/upload.md index f645406..a84a0c5 100644 --- a/wiki/payloadcms/upload.md +++ b/wiki/payloadcms/upload.md @@ -332,8 +332,24 @@ plugins: [ - 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 +## Key Takeaways + +- Enable uploads on any collection via `upload: true` or `upload: { ... }` — Payload auto-adds `filename`, `mimeType`, `filesize` fields +- Upload only works via REST and Local API — **not** GraphQL +- `sharp` must be in Payload config for image resizing (`create-payload-app` adds it by default) +- `withoutEnlargement` defaults to `undefined` → images smaller than a defined size return `null` for that size +- Image cropping happens **before** resizing — all sizes are derived from the cropped image +- Custom Upload components **must** use `` from `@payloadcms/ui` — raw `` causes 400 errors +- Set `disableLocalStorage: true` when using cloud storage adapters (all official adapters do this automatically) +- Restricted file types (`.exe`, `.php`, `.js`, HTML, scripts) are blocked by default unless `mimeTypes` is set or `allowRestrictedFileTypes: true` +- `pasteURL` is enabled by default; use `allowList` for server-side fetching to bypass CORS on external URLs +- Vercel limits server uploads to 4.5 MB — use `clientUploads: true` on the adapter to bypass + ## Related -- [[wiki/payloadcms/configuration|Configuration]] -- [[wiki/payloadcms/fields-complex|Fields Complex]] +- [[wiki/payloadcms/configuration|Payload Config Overview]] +- [[wiki/payloadcms/fields-upload|Upload Field]] - [[wiki/payloadcms/admin-panel-overview|Admin Panel Overview]] +- [[wiki/payloadcms/custom-components-edit-view|Custom Components — Edit View]] +- [[wiki/payloadcms/access-control|Access Control]] +- [[wiki/payloadcms/production-deployment|Production Deployment]]