Cloudinary storage for Payload CMS
A production‑ready Cloudinary adapter for Payload CMS uploads. Includes customizable public IDs, optional versioning with history, PDF thumbnails, Dynamic Folder Mode compatibility, and first‑class media document metadata.
import { buildConfig } from 'payload/config';
import { cloudinaryStorage } from 'payload-cloudinary';
export default buildConfig({
// ... your existing payload config
plugins: [
cloudinaryStorage({
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
},
collections: {
media: true, // Enable for your media collection
},
folder: 'payload-media', // Optional base folder in Cloudinary
}),
],
});import { buildConfig } from 'payload/config';
import { cloudinaryStorage } from 'payload-cloudinary';
export default buildConfig({
// ... your existing payload config
plugins: [
cloudinaryStorage({
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
},
collections: {
media: true, // Enable for your media collection
},
folder: 'payload-media', // Optional base folder in Cloudinary
}),
],
});Installation
Install the package using your preferred package manager:
# With npm
npm install payload-cloudinary
# With yarn
yarn add payload-cloudinary
# With pnpm
pnpm add payload-cloudinary
# With bun
bun add payload-cloudinary# With npm
npm install payload-cloudinary
# With yarn
yarn add payload-cloudinary
# With pnpm
pnpm add payload-cloudinary
# With bun
bun add payload-cloudinaryBasic Configuration
Add the plugin to your Payload config and enable it for your media collection:
import { buildConfig } from 'payload/config';
import { cloudinaryStorage } from 'payload-cloudinary';
export default buildConfig({
// ... your existing payload config
plugins: [
cloudinaryStorage({
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
},
collections: {
media: true, // Enable for your media collection
},
folder: 'payload-media', // Optional base folder in Cloudinary
}),
],
});import { buildConfig } from 'payload/config';
import { cloudinaryStorage } from 'payload-cloudinary';
export default buildConfig({
// ... your existing payload config
plugins: [
cloudinaryStorage({
config: {
cloud_name: process.env.CLOUDINARY_CLOUD_NAME!,
api_key: process.env.CLOUDINARY_API_KEY!,
api_secret: process.env.CLOUDINARY_API_SECRET!,
},
collections: {
media: true, // Enable for your media collection
},
folder: 'payload-media', // Optional base folder in Cloudinary
}),
],
});Custom Fields
Extend your media collection with additional fields. The plugin will merge them into the collection config automatically.
cloudinaryStorage({
// ... other options
customFields: [
{
name: 'alt',
type: 'text',
label: 'Alt Text',
admin: { description: 'Alternative text for accessibility' },
},
{
name: 'caption',
type: 'text',
label: 'Caption',
},
{
name: 'tags',
type: 'array',
label: 'Tags',
fields: [{ name: 'tag', type: 'text', required: true }],
},
],
});cloudinaryStorage({
// ... other options
customFields: [
{
name: 'alt',
type: 'text',
label: 'Alt Text',
admin: { description: 'Alternative text for accessibility' },
},
{
name: 'caption',
type: 'text',
label: 'Caption',
},
{
name: 'tags',
type: 'array',
label: 'Tags',
fields: [{ name: 'tag', type: 'text', required: true }],
},
],
});Public ID Strategy
Control how Cloudinary public IDs are generated for predictable URLs and folder organization.
cloudinaryStorage({
// ... other options
publicID: {
enabled: true, // Enable custom public ID generation
useFilename: true, // Include original filename
uniqueFilename: true,// Add uniqueness to avoid collisions
generatePublicID: (filename, prefix, folder) => {
const sanitizedName = filename
.toLowerCase()
.replace(/\.[^/.]+$/, "")
.replace(/[^a-z0-9]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "").slice(0, 14);
const prefixPath = prefix ? `${prefix}/` : "";
return `${folder}/${prefixPath}${sanitizedName}_${timestamp}`;
},
},
});cloudinaryStorage({
// ... other options
publicID: {
enabled: true, // Enable custom public ID generation
useFilename: true, // Include original filename
uniqueFilename: true,// Add uniqueness to avoid collisions
generatePublicID: (filename, prefix, folder) => {
const sanitizedName = filename
.toLowerCase()
.replace(/\.[^/.]+$/, "")
.replace(/[^a-z0-9]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
const timestamp = new Date().toISOString().replace(/[^0-9]/g, "").slice(0, 14);
const prefixPath = prefix ? `${prefix}/` : "";
return `${folder}/${prefixPath}${sanitizedName}_${timestamp}`;
},
},
});Versioning
Optionally enable versioning to store historical metadata and invalidate old CDN versions when new uploads replace existing files.
cloudinaryStorage({
// ... other options
versioning: {
enabled: true, // Track versions in DB
autoInvalidate: true, // Invalidate old CDN versions
storeHistory: true, // Keep version history metadata
},
});cloudinaryStorage({
// ... other options
versioning: {
enabled: true, // Track versions in DB
autoInvalidate: true, // Invalidate old CDN versions
storeHistory: true, // Keep version history metadata
},
});Dynamic Folder Mode
Newer Cloudinary accounts use Dynamic Folder Mode, where the Media Library’s folders can differ from the public ID path. This plugin supports both modes.
cloudinaryStorage({
// ... other options
supportDynamicFolderMode: true, // Default: true. Matches newer Cloudinary accounts
folder: 'payload-media', // Maps to asset_folder in Dynamic Folder Mode
});cloudinaryStorage({
// ... other options
supportDynamicFolderMode: true, // Default: true. Matches newer Cloudinary accounts
folder: 'payload-media', // Maps to asset_folder in Dynamic Folder Mode
});- Assets appear where you expect in the Media Library UI
- Moving assets in the UI doesn’t break URLs
- Public IDs remain stable and predictable
Frontend Usage
Access Cloudinary metadata (including public_id) directly from your media documents. Build responsive image URLs with Cloudinary transformations and use f_auto/q_auto for best performance.
Troubleshooting
- Make sure your media collection slug matches the plugin config (e.g.
media). - If custom fields don’t appear, restart the dev server and check for field name collisions.
- Ensure your Cloudinary credentials are set and available at build/runtime.
- For Dynamic Folder Mode, verify assets appear in expected folders in the UI.
API Options
Core
config: Cloudinary credentialscollections: enable per collectionfolder: base folder pathenabled: turn plugin on/offdisableLocalStorage: defaults to true
Enhancements
customFields: add fields to media collectionpublicID: customize public ID generationversioning: track history and invalidate CDNsupportDynamicFolderMode: support Cloudinary’s dynamic folders
Found a bug or have a feature request? Open an issue or PR on GitHub.