Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Storage Adapters

Payload offers additional storage adapters to handle file uploads. These adapters allow you to store files in different locations, such as Amazon S3, Vercel Blob Storage, Google Cloud Storage, and more.

Configuration

Storage adapters are registered under the top-level storage key in your Payload config. This placement ensures that upload hooks, static file handlers, and upload instructions are wired up before any plugin modifies the config.

1
import { s3Storage } from '@payloadcms/storage-s3'
2
import { buildConfig } from 'payload'
3
4
export default buildConfig({
5
storage: [
6
s3Storage({
7
bucket: process.env.S3_BUCKET,
8
collections: { media: true },
9
config: { region: process.env.S3_REGION },
10
}),
11
],
12
// ... rest of config
13
})

Upload Instructions

By default, the Admin includes the file in the normal document create or update request. When an adapter's upload instructions provide a better browser upload flow, the Admin uses them instead:

1
Admin
2
-> asks POST /api/upload-instructions how to upload this file
3
4
Payload
5
-> checks access and file limits
6
-> returns the storage adapter's instructions
7
8
Admin
9
-> follows the returned instructions
10
-> creates or updates the document with the completed upload metadata

With clientUploads enabled, S3, Azure, and GCS upload directly to storage instead of through Payload. Providers that need their own SDK return a named instruction handled by their Admin helper. R2 uses the same flow to split the file into smaller requests, even though Payload still handles each chunk.

The upload-instructions endpoint is also available for every upload collection. Other clients, including MCP, can use it when they cannot attach a file to the document request. Without direct provider instructions, Payload returns a temporary upload URL instead. Both paths still run normal validation, image processing, and hooks during document creation.

Vercel Blob Storage

@payloadcms/storage-vercel-blob

Installation

1
pnpm add @payloadcms/storage-vercel-blob

Usage

  • Configure the collections object to specify which collections should use the Vercel Blob adapter. The slug must match one of your existing collection slugs.
  • Ensure you have BLOB_READ_WRITE_TOKEN set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project.
  • When enabled, this package will automatically set disableLocalStorage to true for each collection.
  • When deploying to Vercel, server uploads are limited to 4.5MB. Set clientUploads to true to use upload instructions and send files directly to Vercel Blob.
1
import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
2
import { Media } from './collections/Media'
3
import { MediaWithPrefix } from './collections/MediaWithPrefix'
4
5
export default buildConfig({
6
collections: [Media, MediaWithPrefix],
7
storage: [
8
vercelBlobStorage({
9
enabled: true, // Optional, defaults to true
10
// Specify which collections should use Vercel Blob
11
collections: {
12
media: true,
13
'media-with-prefix': {
14
prefix: 'my-prefix',
15
},
16
},
17
// Token provided by Vercel once Blob storage is added to your Vercel project
18
token: process.env.BLOB_READ_WRITE_TOKEN,
19
}),
20
],
21
})

Configuration Options

Option

Description

Default

enabled

Whether or not to enable the plugin

true

collections

Collections to apply the Vercel Blob adapter to

addRandomSuffix

Must be false or omitted; enabled adapters with configured collections reject true.

false

cacheControlMaxAge

Cache-Control max-age in seconds

365 * 24 * 60 * 60 (1 Year)

token

Vercel Blob storage read/write token

''

clientUploads

Upload directly to Vercel Blob instead of through Payload.

useCompositePrefixes

Always combine collection prefix with document prefix, even when the document prefix already contains it.

false

Migrating Random Suffix Uploads

Remove addRandomSuffix: true or set it to false. Payload records exact storage paths for the current file, original, and stored variants, and uses those paths for reads, rollback, and deletion. Provider-assigned suffixes change these paths after upload and are incompatible with this lifecycle. New managed uploads already use distinct _objectKey folders to avoid overwriting retained objects.

An enabled Vercel Blob adapter with configured collections rejects this option during initialization, before any upload or copy. Explicitly disabled adapters and adapters disabled by a missing token retain their existing behavior. Changing the option does not rename existing blobs; if their actual paths differ from saved file metadata, migrate those paths and metadata together.

S3 Storage

@payloadcms/storage-s3

Installation

1
pnpm add @payloadcms/storage-s3

Usage

  • Configure the collections object to specify which collections should use the S3 Storage adapter. The slug must match one of your existing collection slugs.
  • The config object can be any S3ClientConfig object (from @aws-sdk/client-s3). This is highly dependent on your AWS setup. Check the AWS documentation for more information.
  • When enabled, this package will automatically set disableLocalStorage to true for each collection.
  • When deploying to Vercel, server uploads are limited to 4.5MB. Set clientUploads to true to use upload instructions and send files directly to S3. You must allow CORS PUT requests and the If-None-Match request header from your website.
  • Configure signedDownloads (either globally or per-collection in collections) to use presigned URLs for files downloading. This can improve performance for large files (like videos) while still respecting your access control. Additionally, with signedDownloads.shouldUseSignedURL you can specify a condition whether Payload should use a presigned URL, if you want to use this feature only for specific files.
  • You can conditionally enable the plugin using the enabled option. For example, enabled: Boolean(process.env.S3_BUCKET) skips the plugin in local development when credentials are not set.
1
import { s3Storage } from '@payloadcms/storage-s3'
2
import { Media } from './collections/Media'
3
import { MediaWithPrefix } from './collections/MediaWithPrefix'
4
5
export default buildConfig({
6
collections: [Media, MediaWithPrefix],
7
storage: [
8
s3Storage({
9
collections: {
10
media: true,
11
'media-with-prefix': {
12
prefix,
13
},
14
'media-with-presigned-downloads': {
15
// Filter only mp4 files
16
signedDownloads: {
17
shouldUseSignedURL: ({ collection, filename, req }) => {
18
return filename.endsWith('.mp4')
19
},
20
},
21
},
22
},
23
bucket: process.env.S3_BUCKET,
24
config: {
25
credentials: {
26
accessKeyId: process.env.S3_ACCESS_KEY_ID,
27
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
28
},
29
region: process.env.S3_REGION,
30
// ... Other S3 configuration
31
},
32
}),
33
],
34
})

Configuration Options

Option

Description

Default

enabled

Whether or not to enable the plugin

true

collections

Collections to apply the S3 adapter to

bucket

The name of the S3 bucket

config

S3ClientConfig object passed to the AWS SDK client

acl

Access control list for uploaded files (e.g. 'public-read')

undefined

clientUploads

Upload directly to S3 instead of through Payload.

signedDownloads

Use presigned URLs for file downloads. Can be overridden per collection

useCompositePrefixes

Always combine collection prefix with document prefix, even when the document prefix already contains it.

false

For full S3ClientConfig options, see the AWS SDK Package and S3ClientConfig docs.

Using with Cloudflare R2 (via S3 API)

Cloudflare R2 exposes an S3-compatible API, so you can use @payloadcms/storage-s3 to connect to it. This is the recommended approach when deploying to Vercel, Netlify, or any Node.js environment. (The @payloadcms/storage-r2 adapter is for Cloudflare Workers only, where R2 is available as a native bucket binding.)

1
import { s3Storage } from '@payloadcms/storage-s3'
2
3
export default buildConfig({
4
collections: [Media],
5
storage: [
6
s3Storage({
7
enabled: Boolean(process.env.R2_BUCKET),
8
collections: {
9
media: {
10
disablePayloadAccessControl: true,
11
generateFileURL: ({ filename, prefix }) => {
12
const key = prefix ? `${prefix}/${filename}` : filename
13
return `${process.env.R2_PUBLIC_URL}/${key}`
14
},
15
},
16
},
17
bucket: process.env.R2_BUCKET,
18
config: {
19
credentials: {
20
accessKeyId: process.env.R2_ACCESS_KEY_ID,
21
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
22
},
23
region: 'auto',
24
// R2 S3 API endpoint — for uploads only, not for serving files
25
endpoint: process.env.R2_ENDPOINT,
26
forcePathStyle: true,
27
},
28
}),
29
],
30
})

Required environment variables:

1
R2_BUCKET=my-bucket
2
R2_ACCESS_KEY_ID=...
3
R2_SECRET_ACCESS_KEY=...
4
R2_ENDPOINT=https://<accountId>.r2.cloudflarestorage.com
5
R2_PUBLIC_URL=https://media.yourdomain.com
  • region: 'auto' — required by R2; standard AWS region values are not accepted
  • endpoint — your R2 S3 API endpoint from the Cloudflare dashboard. This is for uploads only — not for serving files
  • forcePathStyle: true — required for R2's path-style bucket addressing
  • R2_PUBLIC_URL — your bucket's public URL (either the *.r2.dev subdomain or a custom domain you've connected in Cloudflare). This is separate from the S3 API endpoint
  • disablePayloadAccessControl: true — bypasses Payload's file proxy so URLs point directly to your public R2 domain

Azure Blob Storage

@payloadcms/storage-azure

Installation

1
pnpm add @payloadcms/storage-azure

Usage

  • Configure the collections object to specify which collections should use the Azure Blob adapter. The slug must match one of your existing collection slugs.
  • When enabled, this package will automatically set disableLocalStorage to true for each collection.
  • When deploying to Vercel, server uploads are limited to 4.5MB. Set clientUploads to true to use upload instructions and send files directly to Azure.
1
import { azureStorage } from '@payloadcms/storage-azure'
2
import { Media } from './collections/Media'
3
import { MediaWithPrefix } from './collections/MediaWithPrefix'
4
5
export default buildConfig({
6
collections: [Media, MediaWithPrefix],
7
storage: [
8
azureStorage({
9
collections: {
10
media: true,
11
'media-with-prefix': {
12
prefix,
13
},
14
},
15
allowContainerCreate:
16
process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true',
17
baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL,
18
connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
19
containerName: process.env.AZURE_STORAGE_CONTAINER_NAME,
20
}),
21
],
22
})

Configuration Options

Option

Description

Default

enabled

Whether or not to enable the plugin

true

collections

Collections to apply the Azure Blob adapter to

allowContainerCreate

Whether or not to allow the container to be created if it does not exist

false

containerAccess

Public access level for a container the plugin creates. 'private' (default) serves blobs only through Payload's access-controlled route; 'blob' / 'container' expose them to anonymous clients directly from Azure. No effect on existing containers.

'private'

baseURL

Base URL for the Azure Blob storage account

connectionString

Azure Blob storage connection string

containerName

Azure Blob storage container name

clientUploads

Upload directly to Azure instead of through Payload.

useCompositePrefixes

Always combine collection prefix with document prefix, even when the document prefix already contains it.

false

Google Cloud Storage

@payloadcms/storage-gcs

Installation

1
pnpm add @payloadcms/storage-gcs

Usage

  • Configure the collections object to specify which collections should use the Google Cloud Storage adapter. The slug must match one of your existing collection slugs.
  • When enabled, this package will automatically set disableLocalStorage to true for each collection.
  • When deploying to Vercel, server uploads are limited to 4.5MB. Set clientUploads to true to use upload instructions and send files directly to GCS.
  • When using clientUploads, configure GCS CORS to allow PUT requests and include x-goog-if-generation-match in responseHeader. Existing client-upload CORS configurations must add this header when upgrading.
1
import { gcsStorage } from '@payloadcms/storage-gcs'
2
import { Media } from './collections/Media'
3
import { MediaWithPrefix } from './collections/MediaWithPrefix'
4
5
export default buildConfig({
6
collections: [Media, MediaWithPrefix],
7
storage: [
8
gcsStorage({
9
collections: {
10
media: true,
11
'media-with-prefix': {
12
prefix,
13
},
14
},
15
bucket: process.env.GCS_BUCKET,
16
options: {
17
apiEndpoint: process.env.GCS_ENDPOINT,
18
projectId: process.env.GCS_PROJECT_ID,
19
},
20
}),
21
],
22
})

Configuration Options

Option

Description

Default

enabled

Whether or not to enable the plugin

true

collections

Collections to apply the storage to

bucket

The name of the bucket to use

options

Google Cloud Storage client configuration. See Docs

acl

Access control list for files that are uploaded

Private

clientUploads

Upload directly to GCS instead of through Payload.

useCompositePrefixes

Always combine collection prefix with document prefix, even when the document prefix already contains it.

false

R2 Storage

@payloadcms/storage-r2

Use this adapter to store uploads in a Cloudflare R2 bucket via the Cloudflare Workers environment. If you're connecting to R2 from a Node.js environment (Vercel, Netlify, etc.) using the S3-compatible API, see Using with Cloudflare R2 (via S3 API) instead.

Installation

1
pnpm add @payloadcms/storage-r2

Usage

  • Configure the collections object to specify which collections should use r2. The slug must match one of your existing collection slugs and be an upload type.
  • Pass in the R2 bucket binding to the bucket option, this should be done in the environment where Payload is running (e.g. Cloudflare Worker).
  • You can conditionally determine whether or not to enable the plugin with the enabled option.
1
export default buildConfig({
2
collections: [Media],
3
storage: [
4
r2Storage({
5
collections: {
6
media: true,
7
},
8
bucket: cloudflare.env.R2,
9
}),
10
],
11
})

Custom Storage Adapters

If you need to create a custom storage adapter, you can use the @payloadcms/plugin-cloud-storage package. This package is used internally by the storage adapters mentioned above.

Installation

pnpm add @payloadcms/plugin-cloud-storage

Usage

Reference any of the existing storage adapters for guidance on how this should be structured. Create an adapter following the GeneratedAdapter interface. Then, pass the adapter to the cloudStorage plugin.

1
type FileOperationArgs = {
2
collection: SanitizedCollectionConfig
3
from: string
4
to: string
5
mimeType?: string
6
req: PayloadRequest
7
}
8
9
export interface GeneratedAdapter {
10
copyFile: (args: FileOperationArgs) => Promise<void>
11
deleteFile: (args: {
12
collection: CollectionConfig
13
req: PayloadRequest
14
storageFilePath: string
15
}) => Promise<void> | void
16
/**
17
* Additional fields to be injected into the base
18
* collection and image sizes
19
*/
20
fields?: Field[]
21
/**
22
* Generates the public URL for a file
23
*/
24
generateURL?: GenerateURL
25
handleDelete: HandleDelete
26
handleUpload: HandleUpload
27
name: string
28
onInit?: () => void
29
moveFile?: (args: FileOperationArgs) => Promise<void>
30
staticHandler: StaticHandler
31
uploadInstructions?: UploadInstructionsCapability
32
}

from and to are complete storage keys, including prefixes. copyFile must leave the source in place, reject an occupied destination, and resolve only after the destination can be read. The optional moveFile method remains available to adapters, but core file rename always copies first and defers source deletion until the document update and its database transaction succeed. The key-based contract applies to path-addressed storage; an adapter using provider-assigned IDs needs a compatible mapping before it can support file versioning.

If a copy creates a destination and then fails its readability check or a post-copy permissions update, the adapter must remove that failed destination before rejecting. Cleanup must target only the object created by that attempt, using a returned generation, version ID, or ETag condition where supported. Never delete an occupied destination after a create-only operation rejects. If cleanup also fails, preserve the original copy error and log the exact remaining storage path for manual cleanup. The first-party adapters follow this contract, including incomplete S3 multipart uploads and Vercel Blob's streamed fallback.

Custom adapters must implement deleteFile for upload rollback and cleanup of unreferenced objects. Its storageFilePath is the complete object path, including prefixes and _objectKey; delete that exact object without requiring a saved document. Failed uploads may need cleanup before a document exists, and version pruning may remove the last document reference before cleanup runs. The plugin rejects enabled adapters without deleteFile during configuration.

handleDelete retains its document-based contract: when it is used for legacy document deletion, it receives the saved file document along with the filename and resolved storage path. An adapter that only needs the storage path can share its deletion implementation between deleteFile and handleDelete, as the first-party adapters do.

An explicit file rename copies stored objects to new current keys and keeps the source readable while the database operation is pending. After the document update and its database transaction succeed, Payload removes only source objects with no remaining document or version references. A failed rename preserves the sources and removes staged copies that no persisted document or version references. This applies with or without versions and regardless of whether the adapter supplies moveFile. A process crash before cleanup may leave extra objects, but committed source references remain readable.

The first-party S3 adapter uses single or multipart server-side copy. The R2 Workers binding supports ordinary uploads and reads without S3 credentials, but copy-dependent file versioning operations require copyCredentials with an R2 account ID, bucket, S3 access key ID, and secret. Azure copies through authenticated source and destination streams, so private sources work but copy traffic passes through the Payload server. GCS uses its provider copy API; Vercel Blob uses its provider copy API with exact destination pathnames. Grant each adapter read and write access to the source and destination, plus object metadata and tag access where used. S3 multipart copy also needs permission to abort incomplete multipart uploads, and copy cleanup needs permission to delete objects (including specific object versions when S3 bucket versioning is enabled); KMS-encrypted S3 objects need the corresponding decrypt and encrypt permissions.

Payload removes versioned upload objects only after their last document or saved version reference is gone and the database operation has committed. If an object deletion fails, Payload logs the complete storage key and leaves the object in place. Before retrying manually, check the current file, original, and stored variants in every current document and retained version. Each representation's filename, prefix, and _objectKey identify its location through the configured adapter's path resolver. Remove the failed object through the storage provider only when none of these locations resolves to its exact key. Failed deletions are not retried automatically.

When providing upload instructions, set uploadInstructions.useInAdmin to true when the Admin panel should use them instead of attaching the whole file to the document request. This is useful for direct-to-storage uploads and for flows such as chunked uploads that still pass through Payload.

staticHandler receives params.operation: 'read' for an ordinary file request (treat a missing value as 'read', for backward compatibility), or 'transform' when Payload is reading the file's bytes on behalf of a file transformer, which only happens when at least one transformer is configured. For 'transform', return the full object body rather than a redirect to a signed/public URL, and skip Range handling, conditional/ETag short-circuiting, and modifyResponseHeaders — Payload applies headers once, on the final response, after every transformer has run. The five first-party adapters already implement this; a custom adapter only needs to branch on operation if it currently redirects to an external URL for 'read'.

1
import { buildConfig } from 'payload'
2
import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage'
3
4
export default buildConfig({
5
plugins: [
6
// cloudStoragePlugin is a low-level building block — place it in plugins, not storage.
7
// The built-in adapter packages (s3Storage, gcsStorage, etc.) wrap this internally
8
// and are used via storage instead.
9
cloudStoragePlugin({
10
collections: {
11
'my-collection-slug': {
12
adapter: theAdapterToUse, // see docs for the adapter you want to use
13
},
14
},
15
}),
16
],
17
// The rest of your config goes here
18
})

Plugin options

This plugin is configurable to work across many different Payload collections. A * denotes that the property is required.

Option

Type

Description

collections *

Record<string, CollectionOptions>

Object with keys set to the slug of collections you want to enable the plugin for, and values set to collection-specific options.

enabled

boolean

To conditionally enable/disable plugin. Default: true.

Collection-specific options

Option

Type

Description

adapter *

Adapter

Pass in the adapter that you'd like to use for this collection. You can also set this field to null for local development if you'd like to bypass cloud storage in certain scenarios and use local storage.

disableLocalStorage

boolean

Choose to disable local storage on this collection. Defaults to true.

disablePayloadAccessControl

true

Set to true to disable Payload's Access Control. More

generateFileURL

GenerateFileURL

Override the generated file URL with one that you create.

prefix

string

Set to media/images to upload files inside media/images folder in the bucket.

Prefix Composition

Storage adapters support two types of prefixes:

  • Collection prefix: Set at the adapter configuration level (e.g., prefix: 'media-folder')
  • Document prefix: Set per-document via the prefix field on the upload collection

New uploads are always written within the configured collection prefix. By default, a document prefix that is already within the collection prefix is used as-is. Any other document prefix is nested beneath the collection prefix.

With useCompositePrefixes: true, the collection and document prefixes are always combined. This differs when the document prefix already contains the collection prefix:

1
# Without useCompositePrefixes (default)
2
Collection prefix: uploads
3
Document prefix: uploads/user-123
4
Result: uploads/user-123/image.jpg
5
6
# With useCompositePrefixes: true
7
Collection prefix: uploads
8
Document prefix: uploads/user-123
9
Result: uploads/uploads/user-123/image.jpg

Persisted paths remain backward compatible. Existing files keep their stored prefixes for reads, generated URLs, replacement cleanup, and deletion, even when those paths are outside the configured collection prefix. Replacing an existing file writes the replacement within the collection prefix and cleans up the previous stored location.

Upgrading does not move existing objects. If another system relies on historical object paths, plan a separate migration before changing or replacing those files. New uploads and replacements may persist a normalized prefix that differs from legacy metadata.

Custom adapters using getFilePrefix from @payloadcms/plugin-cloud-storage/utilities should forward the static handler's access-checked doc to the helper. Its prefixQueryParam option is deprecated and ignored, but remains accepted for TypeScript compatibility. Without a document, the helper uses the upload reference or looks up the stored prefix by filename. Direct callers must perform their own access checks before supplying a document.

1
storage: [
2
s3Storage({
3
collections: {
4
media: {
5
prefix: 'uploads', // All files go under uploads/
6
},
7
},
8
useCompositePrefixes: true, // Document prefixes append to collection prefix
9
bucket: process.env.S3_BUCKET,
10
// ...
11
}),
12
],

Payload Access Control

Payload ships with Access Control that runs even on statically served files. The same read Access Control property on your upload-enabled collections is used, and it allows you to restrict who can request your uploaded files.

To preserve this feature, by default, this plugin keeps all file URLs exactly the same. Your file URLs won't be updated to point directly to your cloud storage source, as in that case, Payload's Access control will be completely bypassed and you would need public readability on your cloud-hosted files.

Instead, all uploads will still be reached from the default /collectionSlug/staticURL/filename path. This plugin will "pass through" all files that are hosted on your third-party cloud service—with the added benefit of keeping your existing Access Control in place.

If this does not apply to you (your upload collection has read: () => true or similar) you can disable this functionality by setting disablePayloadAccessControl to true. When this setting is in place, this plugin will update your file URLs to point directly to your cloud host. For a concrete example, see Using with Cloudflare R2 (via S3 API).

Conditionally Enabling/Disabling

The proper way to conditionally enable/disable this plugin is to use the enabled property.

1
cloudStoragePlugin({
2
enabled: process.env.MY_CONDITION === 'true',
3
collections: {
4
'my-collection-slug': {
5
adapter: theAdapterToUse, // see docs for the adapter you want to use
6
},
7
},
8
}),

Was this page helpful?

Next

File Transformers