Skip to main content
Client uploads send the file bytes straight from the browser to Bunny. The Payload server only mints a short-lived, signed upload URL — it never receives the file itself. This removes serverless body-size limits (Vercel caps request bodies at ~4.5 MB) and keeps large uploads off your app server entirely. They are configured under storage.clientUploads:
payload.config.ts
Client uploads cover Bunny Storage files. For large Stream videos, use TUS resumable uploads instead — TUS is the browser-direct path for Stream.

How it works

1

Request

When an editor picks a file, the admin UI calls POST /api/storage-bunny/storage/upload with { collectionSlug, filename, filesize, mimeType }.
2

Authorize and validate

The server runs the access check (default: any authenticated user), then validates the file against the collection’s upload.mimeTypes, Payload’s upload.limits.fileSize, and — for edge transport — edge.maxSize.
3

Resolve the path

The prefix callback (or the collection’s static prefix) decides where the file lands, server-side. When you use a prefix callback, the plugin persists the resolved prefix onto the created document (and injects the hidden per-document prefix field for you), so later reads, deletes, and URL generation all resolve to the same path.
4

Mint the URL

The server returns a presigned (S3) or signed (edge) upload URL scoped to that path.
5

Upload

The browser PUTs the file straight to Bunny. Your app server is never in the data path.
This is built on @payloadcms/plugin-cloud-storage’s client-uploads mechanism.

Transport is automatic

The plugin picks the transport from the zone:
  • storage.s3 is set → presigned S3. The browser PUTs straight to Bunny’s S3 endpoint. No Edge Script is involved, and clientUploads.edge isn’t applicable.
  • storage.s3 isn’t set → edge. The upload is proxied through a Bunny Edge Script, so clientUploads.edge is required.
A collection that points at its own storage zone follows that zone’s transport: its own s3 presigns against its own endpoint, its own edge mints against its own script.

Client upload options

Pass storage.clientUploads: true for defaults, false to disable, or an object:
When the zone sets storage.s3, the clientUploads object accepts only access and prefix — the edge block isn’t available for S3 zones. This is enforced by the type: a storage zone is either HTTP API (needs edge) or S3 (presigns directly).

S3 transport

When the zone has storage.s3, the browser sends a presigned PUT straight to Bunny’s S3 endpoint. This requires a storage zone created with S3 compatibility enabled.
payload.config.ts
No Edge Script to deploy: the presigned URL is signed with your existing storage credentials.

Edge transport

Without storage.s3, the file is proxied through a Bunny Edge Script that you deploy once with bunny:deploy-edge-script. clientUploads.edge is required.
payload.config.ts
The server mints a signed HMAC URL for each upload: it appends an X-Upload-Expires timestamp, an X-Upload-Zone name, and an HMAC-SHA256 signature over the request. The Edge Script rejects anything whose signature doesn’t verify against its SHARED_SECRET, so edge.secret must match the secret the script was deployed with. Each zone’s storage credentials live in their own ZONE_<NAME> script secret, so zones are added and removed independently — re-deploying to add a zone never touches the other zones’ secrets or the shared SHARED_SECRET.

Access control

access gates who may request an upload URL. It receives { collectionSlug, req } and returns a boolean (or a promise). It runs before the prefix callback, so an unauthorized caller never reaches path resolution.
The default requires an authenticated user (req.user must be set). The endpoint also enforces the collection’s upload.mimeTypes and Payload’s upload.limits.fileSize regardless of your access function.

Client upload requirements

The config validator throws a startup error if any of these is violated:
  • Bunny Storage must be enabled for the collection.
  • A zone with storage.s3 presigns directly — no edge config, nothing to deploy.
  • A zone without storage.s3 requires storage.clientUploads.edge.scriptUrl and edge.secret.
  • All non-s3 zones that share the same edge.scriptUrl must use the same edge.secret — one deployed script holds one shared secret.

Per-collection client uploads

Set storage.clientUploads per collection to enable, disable, or reconfigure browser-direct uploads. A collection inherits the global storage.clientUploads unless it sets its own:
payload.config.ts
See Collection overrides for the full list of options you can tune per collection, and Examples for complete configs.