vault backup: 2026-05-15 16:54:04

This commit is contained in:
Vadym Samoilenko 2026-05-15 16:54:04 +01:00
parent 45f1e250f9
commit 57d13f19c1
3 changed files with 251 additions and 2 deletions

View file

@ -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://<accountId>.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 `/<collectionSlug>/<staticURL>/<filename>` 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

View file

@ -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 `<Upload>` from `@payloadcms/ui` — a raw `<input type="file">` 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 `<Upload>` from `@payloadcms/ui` — raw `<input type="file">` 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]]