Store and serve media files from your Payload CMS using Bunny's fast global CDN.
Built on top of @payloadcms/plugin-cloud-storage
for seamless Payload CMS integration.
- Features
- Performance Tip
- Installation
- Quick Start
- Configuration
- CDN Cache Management
- Getting API Keys
- Storage Regions
- Examples
- Upload files to Bunny Storage
- Handle videos with Bunny Stream (HLS, MP4, thumbnails)
- TUS resumable uploads for large video files
- Show thumbnails in admin panel
- Signed URLs for secure file access
- Custom URL transformations
- Control file access with Payload rules or direct CDN links
- Automatic CDN cache purging when files change
Set
disablePayloadAccessControl: true
for best performance.This lets users download files directly from Bunny's CDN servers instead of through your Payload server, making content delivery much faster.
Requires Payload CMS 3.53.0 or higher.
Migration from v1.x: Version 2.0.0+ requires Payload CMS 3.53.0+. For older Payload versions (3.0.0 - 3.52.x), use version 1.x with the optional experimental
replaceSaveButtonComponent
feature to fix the field update bug. See Migration Guide for detailed upgrade instructions.
# npm
npm install @seshuk/payload-storage-bunny
# yarn
yarn add @seshuk/payload-storage-bunny
# pnpm
pnpm add @seshuk/payload-storage-bunny
Create a .env
file in your project root:
BUNNY_STORAGE_API_KEY=your-storage-api-key
BUNNY_HOSTNAME=files.example.b-cdn.net
BUNNY_ZONE_NAME=your-storage-zone
Add the plugin to your Payload config:
import { buildConfig } from 'payload'
import { bunnyStorage } from '@seshuk/payload-storage-bunny'
export default buildConfig({
plugins: [
bunnyStorage({
collections: {
media: {
prefix: 'media',
disablePayloadAccessControl: true, // Use direct CDN access
},
},
storage: {
apiKey: process.env.BUNNY_STORAGE_API_KEY,
hostname: process.env.BUNNY_HOSTNAME,
zoneName: process.env.BUNNY_ZONE_NAME,
},
}),
],
})
Define a collection that uses the plugin:
import { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: {
disableLocalStorage: true, // Required - handled by Bunny.net
},
fields: [
{
name: 'alt',
type: 'text',
},
],
}
Important: When you use this plugin,
disableLocalStorage
is automatically set totrue
for each collection. Files won't be stored on your server.
Define which collections will use Bunny Storage:
Option | Type | Default | Description |
---|---|---|---|
[collectionSlug] |
boolean | object |
- | Enable Bunny Storage for collection (true) or with options |
prefix |
string |
Collection slug | Folder prefix within Bunny Storage |
disablePayloadAccessControl |
boolean |
false |
Use direct CDN access (bypasses Payload auth) |
adminThumbnail |
boolean | object |
Global setting | Override global admin thumbnail config |
signedUrls |
boolean | object |
Global setting | Override global signed URLs config |
urlTransform |
object |
Global setting | Override global URL transform config |
stream.thumbnailTime |
number |
Global setting | Override default thumbnail time for videos |
Simple usage:
collections: {
media: true, // Enable with defaults
videos: { prefix: 'video-uploads', disablePayloadAccessControl: true }
}
The prefix
option organizes files in folders within your Bunny Storage. For example, prefix: 'images'
stores uploads in an "images" folder.
Connect to Bunny Storage:
Option | Type | Required | Description |
---|---|---|---|
apiKey |
string |
✅ | Your Bunny Storage API key |
hostname |
string |
✅ | Your CDN domain from Pull Zone (e.g., 'files.example.b-cdn.net') |
zoneName |
string |
✅ | Your storage zone name |
region |
string |
❌ | Storage region code ('uk', 'ny', 'la', 'sg', 'se', 'br', 'jh', 'syd') |
tokenSecurityKey |
string |
❌ | Security key for signing storage URLs |
uploadTimeout |
number |
❌ | Upload timeout in milliseconds (default: 120000) |
Important: Bunny Storage requires a Pull Zone to be configured for your Storage Zone. Files will not be accessible without a properly configured Pull Zone. The
hostname
should be your Pull Zone hostname, not the Storage API endpoint. See Bunny's documentation on accessing and delivering files from Bunny Storage.
Optional settings for video handling:
Option | Type | Required | Description |
---|---|---|---|
apiKey |
string |
✅ | Your Bunny Stream API key |
hostname |
string |
✅ | Stream CDN domain (e.g., 'vz-example-123.b-cdn.net') |
libraryId |
number |
✅ | Your video library ID (e.g., 123456) |
mp4Fallback |
boolean |
❌ | Enable MP4 downloads (required with access control) |
thumbnailTime |
number |
❌ | Default thumbnail time in milliseconds |
tokenSecurityKey |
string |
❌ | Security key for signing stream URLs |
uploadTimeout |
number |
❌ | Upload timeout in milliseconds (default: 300000) |
tus |
boolean | object |
❌ | Enable TUS resumable uploads (see TUS config below) |
cleanup |
boolean | object |
❌ | Automatic cleanup of incomplete uploads (requires Jobs Queue setup) |
Option | Type | Default | Description |
---|---|---|---|
autoMode |
boolean |
true |
Auto-enable TUS for supported MIME types |
checkAccess |
function |
Built-in check | Custom authorization function |
mimeTypes |
string[] |
Video/audio types | Supported MIME types for TUS uploads |
uploadTimeout |
number |
3600 |
Upload timeout in seconds |
Option | Type | Default | Description |
---|---|---|---|
maxAge |
number |
86400 |
Time in seconds after which incomplete uploads are considered dead |
schedule |
object |
{ cron: '0 2 * * *', queue: 'storage-bunny' } |
Cron schedule for cleanup task |
Note: Cleanup feature requires Jobs Queue to be configured in your Payload setup. See Payload Jobs Queue documentation for setup instructions.
Note: If you use Payload's access control, you must enable MP4 fallback both here and in your Bunny Stream settings.
Important: Video support works even without Bunny Stream configured. If Bunny Stream is disabled, video files upload to Bunny Storage like any other file. Bunny Stream adds enhanced video features (streaming, adaptive bitrates, thumbnails).
Enable automatic CDN cache purging for storage files (not applicable to Stream):
Option | Type | Required | Description |
---|---|---|---|
enabled |
boolean |
✅ | Enable cache purging |
apiKey |
string |
✅ | Your Bunny API key for purging operations |
async |
boolean |
❌ | Wait for purge to complete (default: false) |
When enabled, the plugin automatically purges CDN cache after:
- File uploads
- File deletions
This ensures visitors always see the most up-to-date files, especially important when replacing existing files (like during image cropping).
Control thumbnails in admin panel:
Option | Type | Default | Description |
---|---|---|---|
enabled |
boolean |
true |
Enable admin thumbnails |
appendTimestamp |
boolean |
false |
Add timestamp to bust cache |
queryParams |
object |
{} |
Custom image parameters (works with Bunny Optimizer) |
Example queryParams:
queryParams: {
width: '300',
height: '300',
quality: '90'
}
When appendTimestamp
is enabled, the plugin automatically adds a timestamp parameter to image URLs in the admin panel. This ensures updated files show the latest version without browser caching issues.
The queryParams
option works great with Bunny's Image Optimizer service, letting you resize, crop, and optimize images on-the-fly.
Enable signed URLs for secure file access:
Option | Type | Default | Description |
---|---|---|---|
expiresIn |
number |
7200 |
Link expiration time in seconds |
allowedCountries |
string[] |
[] |
Allowed countries (ISO 3166-1 alpha-2 codes) |
blockedCountries |
string[] |
[] |
Blocked countries (ISO 3166-1 alpha-2 codes) |
shouldUseSignedUrl |
function |
Always sign | Custom function to determine when to use signed URLs |
staticHandler |
object |
{} |
Static handler behavior (see below) |
When Payload access control is enabled, files can be served in two ways:
- Proxying (default): Payload downloads the file from Bunny and serves it to the user
- Redirect: Payload generates a signed URL and redirects the user to download directly from Bunny
Redirect is recommended as it reduces server load and improves performance by avoiding file proxying.
Option | Type | Default | Description |
---|---|---|---|
useRedirect |
boolean |
false |
Redirect instead of proxying content (recommended for better performance) |
redirectStatus |
number |
302 |
HTTP status code for redirects (301, 302, 307, 308) |
expiresIn |
number |
Uses main expiresIn |
Override expiration time for redirects |
Signed URLs work with both Storage and Stream when tokenSecurityKey
is configured.
Custom URL transformations for complete control over file URLs:
Option | Type | Default | Description |
---|---|---|---|
appendTimestamp |
boolean |
false |
Add timestamp parameter to URLs |
queryParams |
object |
{} |
Static query parameters to append |
Option | Type | Description |
---|---|---|
transformUrl |
function |
Custom function for complete URL control |
Transform function signature:
;(args: {
baseUrl: string
collection: CollectionConfig
data?: Record<string, unknown>
filename: string
prefix?: string
}) => string
Note: URL transforms don't work when
disablePayloadAccessControl
is true for the collection.
TUS (resumable uploads) enables reliable uploads of large video files by breaking them into chunks and allowing resume if the connection is interrupted.
Why use TUS uploads?
- Large files: Essential for video files over 100MB
- Unreliable connections: Automatically resumes interrupted uploads
- Serverless environments: Perfect for platforms like Vercel with request timeout and file size limits
- Better UX: Users don't lose progress if something goes wrong
Upload Modes:
- Auto mode (
autoMode: true
): TUS is automatically enabled for supported video/audio files - Manual mode (
autoMode: false
): Admin UI shows a toggle button to switch between standard and TUS uploads
Simple enable: tus: true
(uses auto mode by default)
Detailed configuration: See TUS Upload Configuration table in Stream Configuration section above.
Important: The TUS
mimeTypes
setting works together with your collection'smimeTypes
setting. If a file type is allowed in TUS config but blocked in your collection config, the collection setting takes priority and the file will be rejected.
collections: {
media: {
// Optional folder prefix
prefix: 'media',
// How files are accessed
disablePayloadAccessControl: true
}
}
If disablePayloadAccessControl
is not true
:
- Files go through Payload's API
- Your access rules work
- Videos need MP4 fallback enabled (unless using signed URLs with redirect)
- MP4s are served instead of HLS
- Good for files that need protection
- Performance tip: Use signed URLs with redirect (configured in Static Handler Configuration) to avoid MP4 fallback and reduce server load
When disablePayloadAccessControl: true
:
- Files go directly from Bunny CDN
- No access rules
- Videos use HLS streams (
playlist.m3u8
) - Faster delivery but open access
- No need for MP4 fallback
There are two approaches to managing CDN cache for your Bunny Storage files:
Enable automatic cache purging when files are uploaded or deleted:
purge: {
enabled: true,
apiKey: process.env.BUNNY_API_KEY,
async: false // Wait for purge to complete (default: false)
}
This is the most comprehensive approach as it ensures CDN cache is immediately purged when files change, making updated content available to all visitors.
For the admin panel specifically, you can use timestamp-based cache busting:
- First, configure the plugin to add timestamps to image URLs:
adminThumbnail: {
appendTimestamp: true
}
- In your Bunny Pull Zone settings:
- Go to the "Caching" section
- Enable "Vary Cache" for "URL Query String"
- Add "t" to the "Query String Vary Parameters" list
This approach only affects how images display in the admin panel and doesn't purge the actual CDN cache. It appends a timestamp parameter (?t=1234567890
) to image URLs, causing Bunny CDN to treat each timestamped URL as a unique cache entry.
Choose the approach that best fits your needs:
- Use automatic cache purging for immediate updates everywhere
- Use timestamp-based cache busting for a simpler setup that only affects the admin panel
New to Bunny.net? Sign up here to get started with fast global CDN and streaming services.
To find your Bunny Storage API key:
- Go to your Bunny Storage dashboard
- Click on your Storage Zone
- Go to "FTP & API Access" section
- Use the "Password" field as your API key (important: you must use the full Password, not the Read-only password as it won't work for uploads)
- Your "Username" is your storage zone name (use this for the
zoneName
parameter) - The "Hostname" value can help determine your
region
(e.g., if it showsny.storage.bunnycdn.com
, your region isny
)
Remember that the hostname
parameter in the plugin configuration should come from your Pull Zone, not from this section.
To find your Bunny Stream API key:
- Go to your Bunny Stream dashboard
- Select your library
- Click on "API" in the sidebar
- Find "Video Library ID" for your
libraryId
setting (like "123456") - Find "CDN Hostname" for your
hostname
setting (like "vz-example-123.b-cdn.net") - The "API Key" is found at the bottom of the page
To find your Bunny API key (used for cache purging):
- Go to your Bunny.net dashboard
- Click on your account in the top-right corner
- Select "Account settings" from the dropdown menu
- Click on "API" in the sidebar menu
- Copy the API key displayed on the page
Token security keys are used for signed URLs to provide secure access to files.
To get your Storage token security key:
- Go to your Bunny.net dashboard
- Navigate to Delivery → CDN
- Select your Pull Zone
- Click on Security in the sidebar
- Click on Token Authentication
- Enable "Token authentication"
- Copy the Url token authentication Key
To get your Stream token security key:
- Go to your Bunny.net dashboard
- Navigate to Delivery → Stream
- Select your Video Library
- Click on API in the sidebar
- Find CDN zone management section
- Click Manage button
- Click on Security in the sidebar
- Click on Token Authentication
- Enable "Token authentication"
- Copy the Url token authentication Key
Choose where to store your files. If you don't pick a region, the default storage location is used.
Use only the region code in the region
setting:
- Default: leave empty
uk
- London, UKny
- New York, USla
- Los Angeles, USsg
- Singaporese
- Stockholm, SEbr
- São Paulo, BRjh
- Johannesburg, SAsyd
- Sydney, AU
To determine your region, check your Bunny Storage Zone settings. Pick a region closest to your users for best performance. The region code is found in your Storage Zone's hostname (e.g., if your endpoint is ny.storage.bunnycdn.com
, use ny
as the region).
Example:
storage: {
apiKey: process.env.BUNNY_STORAGE_API_KEY,
hostname: 'assets.example.b-cdn.net',
region: 'ny', // Just 'ny', not 'ny.storage.bunnycdn.com'
zoneName: 'my-zone'
}
import { buildConfig } from 'payload'
import { bunnyStorage } from '@seshuk/payload-storage-bunny'
export default buildConfig({
plugins: [
bunnyStorage({
collections: {
media: {
prefix: 'media',
disablePayloadAccessControl: true,
},
},
storage: {
apiKey: process.env.BUNNY_STORAGE_API_KEY,
hostname: 'files.example.b-cdn.net',
zoneName: 'your-storage-zone',
},
// Optional: Enable video streaming
stream: {
apiKey: process.env.BUNNY_STREAM_API_KEY,
hostname: 'vz-example-123.b-cdn.net',
libraryId: 123456,
tus: true, // Enable resumable uploads
},
// Optional: Auto-purge CDN cache
purge: {
enabled: true,
apiKey: process.env.BUNNY_API_KEY,
},
}),
],
})
For detailed configuration examples and advanced use cases, see docs/examples.md.
This project is licensed under the MIT License - see the LICENSE file for details.
Need help? Here are some resources:
- Documentation: Bunny.net Documentation
- Bug Reports: GitHub Issues
- Community Support: Payload CMS Discord
- Questions: Join the discussion in our GitHub Issues or Payload Discord
Built with ❤️ for the Payload CMS community.