> ## Documentation Index
> Fetch the complete documentation index at: https://payload-storage-bunny.seshuk.im/llms.txt
> Use this file to discover all available pages before exploring further.

# Examples

> Copy-ready bunnyStorage configurations for storage, streaming, signed URLs, thumbnails, client uploads, and multi-collection setups.

Copy-ready configurations for common setups. Each one is a complete `bunnyStorage(...)` call — combine pieces as needed for your project. For option-by-option reference, see [Configuration](/configuration/overview).

<Tip>
  Starting from scratch? The [setup wizard](/cli/init) provisions your Bunny resources and prints a ready-to-paste
  config plus `.env` lines. Run it first, then use these examples to tune the result.
</Tip>

## Basic setup

### Storage only

```ts payload.config.ts theme={null}
import { buildConfig } from 'payload'
import { bunnyStorage } from '@seshuk/payload-storage-bunny'

export default buildConfig({
  plugins: [
    bunnyStorage({
      collections: {
        media: true,
      },
      storage: {
        apiKey: process.env.BUNNY_STORAGE_API_KEY,
        hostname: 'example.b-cdn.net',
        zoneName: 'my-zone',
      },
    }),
  ],
})
```

### With cache purging

```ts theme={null}
bunnyStorage({
  accountApiKey: process.env.BUNNY_ACCOUNT_API_KEY, // required for purge
  collections: {
    media: true,
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
  },
  purge: true,
})
```

See [Cache purging](/configuration/cache-purge).

## Streaming video

### Direct CDN vs Payload access control

Two ways to serve Stream videos, depending on whether the collection uses Payload's access control.

<Tabs>
  <Tab title="Direct CDN access (fastest)">
    Files bypass Payload's access control and stream straight from Bunny.

    ```ts theme={null}
    bunnyStorage({
      accountApiKey: process.env.BUNNY_ACCOUNT_API_KEY,
      collections: {
        media: {
          prefix: 'uploads',
          disablePayloadAccessControl: true,
        },
      },
      storage: {
        apiKey: process.env.BUNNY_STORAGE_API_KEY,
        hostname: 'example.b-cdn.net',
        region: 'ny',
        zoneName: 'my-zone',
      },
      stream: {
        apiKey: process.env.BUNNY_STREAM_API_KEY,
        hostname: 'vz-abc123def-456.b-cdn.net',
        libraryId: 123456,
        thumbnailTime: 5000,
        tus: true,
      },
      purge: true,
    })
    ```
  </Tab>

  <Tab title="Payload access control">
    Files go through Payload's own access rules. Stream videos need `mp4Fallback` or signed URLs with redirect — see [Access control and mp4Fallback](/configuration/stream/overview#access-control-and-mp4-fallback).

    ```ts theme={null}
    bunnyStorage({
      accountApiKey: process.env.BUNNY_ACCOUNT_API_KEY,
      collections: {
        media: {
          prefix: 'uploads',
          signedUrls: {
            expiresIn: 3600,
            allowedCountries: ['US', 'CA', 'GB'],
            staticHandler: { useRedirect: true },
          },
        },
      },
      storage: {
        apiKey: process.env.BUNNY_STORAGE_API_KEY,
        hostname: 'example.b-cdn.net',
        region: 'ny',
        zoneName: 'my-zone',
        tokenSecurityKey: process.env.BUNNY_STORAGE_TOKEN_SECURITY_KEY,
      },
      stream: {
        apiKey: process.env.BUNNY_STREAM_API_KEY,
        hostname: 'vz-abc123def-456.b-cdn.net',
        libraryId: 123456,
        thumbnailTime: 5000,
        tokenSecurityKey: process.env.BUNNY_STREAM_TOKEN_SECURITY_KEY,
        tus: {
          autoMode: true,
          expiresIn: 3600,
        },
        cleanup: {
          maxAge: 86400,
          schedule: { cron: '0 2 * * *', queue: 'storage-bunny' },
        },
      },
      purge: true,
    })
    ```
  </Tab>
</Tabs>

### TUS resumable uploads for large videos

```ts theme={null}
bunnyStorage({
  collections: {
    videos: {
      prefix: 'video-content',
      disablePayloadAccessControl: true,
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
    uploadTimeout: 300000,
  },
  stream: {
    apiKey: process.env.BUNNY_STREAM_API_KEY,
    hostname: 'vz-abc123def-456.b-cdn.net',
    libraryId: 123456,
    uploadTimeout: 600000,
    tus: {
      autoMode: true,
      expiresIn: 7200, // 2 hours for large files
      checkAccess: (req) => req.user?.role === 'admin' || req.user?.role === 'editor',
    },
    cleanup: true,
  },
})
```

See [Stream: TUS resumable uploads](/configuration/stream/tus).

### Stream only, no storage

```ts theme={null}
bunnyStorage({
  collections: {
    videos: {
      prefix: 'videos',
      disablePayloadAccessControl: true,
    },
  },
  stream: {
    apiKey: process.env.BUNNY_STREAM_API_KEY,
    hostname: 'vz-abc123def-456.b-cdn.net',
    libraryId: 123456,
    mp4Fallback: true,
    thumbnailTime: 2000,
    tus: {
      autoMode: true,
      expiresIn: 3600,
    },
    cleanup: {
      maxAge: 86400,
    },
  },
  thumbnail: true,
})
```

### Stream webhook for encoding status

Bunny encodes MP4 renditions asynchronously. Point your Stream library's webhook at the plugin so that, once encoding finishes, it fills in `bunnyData.stream.resolutions`. The `secret` is your library's **Read-Only API key**, which Bunny signs each webhook with.

```ts theme={null}
bunnyStorage({
  collections: { media: true },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
  },
  stream: {
    apiKey: process.env.BUNNY_STREAM_API_KEY,
    hostname: 'vz-abc123def-456.b-cdn.net',
    libraryId: 123456,
    mp4Fallback: true, // resolutions are populated once encoding completes
    webhook: {
      secret: process.env.BUNNY_STREAM_READONLY_API_KEY, // library Read-Only API key
    },
  },
})
```

In your Bunny Stream library settings, set the webhook URL to `https://your-site.com/api/storage-bunny/stream/webhook` (no query secret). See [Webhooks](/configuration/stream/webhooks).

## Secure access

### Signed URLs

```ts theme={null}
bunnyStorage({
  collections: {
    media: {
      signedUrls: {
        expiresIn: 7200,
        allowedCountries: ['US', 'CA', 'GB', 'AU'],
        shouldUseSignedUrl: ({ filename }) => /\.(mp4|webm|mov|avi)$/i.test(filename),
        staticHandler: {
          useRedirect: true,
          redirectStatus: 302,
          expiresIn: 1800,
        },
      },
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
    tokenSecurityKey: process.env.BUNNY_STORAGE_TOKEN_SECURITY_KEY,
  },
  signedUrls: {
    expiresIn: 7200,
    blockedCountries: ['CN', 'RU'],
  },
})
```

See [Signed URLs](/configuration/signed-urls).

### IP-locked signed URLs

Bind each signed link to the requesting client's IPv4 address, so a leaked URL stops working from any other IP. You supply the `userIp` callback because extracting the real client IP depends on your proxy chain.

```ts theme={null}
bunnyStorage({
  collections: {
    media: {
      signedUrls: {
        expiresIn: 3600,
        staticHandler: { useRedirect: true }, // userIp runs for redirect links
        userIp: ({ req }) => {
          // Pick the header your proxy/CDN sets for the real client IP.
          const forwarded = req.headers.get('x-forwarded-for')
          return forwarded?.split(',')[0]?.trim() || undefined
        },
      },
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
    tokenSecurityKey: process.env.BUNNY_STORAGE_TOKEN_SECURITY_KEY,
  },
})
```

<Warning>
  Enforcement requires **Token IP Validation** enabled on the Bunny zone/library, and only **IPv4** is supported. See
  [IP locking](/configuration/signed-urls#ip-locking-userip).
</Warning>

## URLs and thumbnails

### Custom URL transforms

```ts theme={null}
bunnyStorage({
  collections: {
    media: {
      urlTransform: {
        appendTimestamp: true,
        queryParams: { version: '2' },
      },
    },
    avatars: {
      urlTransform: {
        transformUrl: ({ baseUrl, filename }) => {
          const size = filename.includes('large') ? '300' : '150'
          return `${baseUrl}?width=${size}&height=${size}&quality=90`
        },
      },
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
  },
  thumbnail: {
    appendTimestamp: true,
    queryParams: { width: '200', height: '200', quality: '85' },
  },
})
```

See [URL transforms](/configuration/url-transforms) and [Thumbnails](/configuration/thumbnails).

### Named-size thumbnails

```ts payload.config.ts theme={null}
export default buildConfig({
  collections: [
    {
      slug: 'media',
      upload: {
        imageSizes: [
          { name: 'thumbnail', width: 150, height: 150 },
          { name: 'preview', width: 400, height: 300 },
        ],
      },
      fields: [{ name: 'alt', type: 'text', required: true }],
    },
  ],
  plugins: [
    bunnyStorage({
      collections: {
        media: {
          thumbnail: {
            sizeName: 'thumbnail',
            appendTimestamp: true,
          },
        },
      },
      storage: {
        apiKey: process.env.BUNNY_STORAGE_API_KEY,
        hostname: 'example.b-cdn.net',
        zoneName: 'my-zone',
      },
    }),
  ],
})
```

## Client uploads

### Browser-direct client uploads

Send file bytes straight from the browser to Bunny, bypassing the Payload server — useful on serverless hosts with tight body-size limits and for large files. Configure it under `storage.clientUploads`; the transport is chosen automatically from the zone.

<Tabs>
  <Tab title="S3 zone">
    Presigned PUT straight to Bunny's S3 endpoint. Requires a storage zone created with S3 compatibility — no Edge Script to deploy.

    ```ts theme={null}
    bunnyStorage({
      collections: { media: true },
      storage: {
        apiKey: process.env.BUNNY_STORAGE_API_KEY,
        hostname: 'example.b-cdn.net',
        zoneName: 'my-zone',
        s3: { region: 'de' },
        clientUploads: true, // presigned S3, because storage.s3 is set
      },
    })
    ```
  </Tab>

  <Tab title="HTTP API zone (edge)">
    Proxies the upload through a Bunny Edge Script. Works with any storage zone.

    ```ts theme={null}
    bunnyStorage({
      collections: { media: true },
      storage: {
        apiKey: process.env.BUNNY_STORAGE_API_KEY,
        hostname: 'example.b-cdn.net',
        zoneName: 'my-zone',
        clientUploads: {
          edge: {
            scriptUrl: 'https://my-uploader.b-cdn.net',
            secret: process.env.BUNNY_EDGE_UPLOAD_SECRET,
          },
        },
      },
    })
    ```

    Deploy the Edge Script first with `npx payload bunny:deploy-edge-script` — it prints the shared `edge: { scriptUrl, secret }` block to paste here.
  </Tab>
</Tabs>

See [Client uploads](/configuration/storage/client-uploads).

### Multiple zones behind one Edge Script

One deployed Edge Script serves every non-s3 zone. Paste the **same** `edge: { scriptUrl, secret }` (printed by `npx payload bunny:deploy-edge-script`) into each non-s3 zone that enables client uploads; s3 zones presign directly and need no edge config.

```ts theme={null}
bunnyStorage({
  collections: {
    media: true, // uses the global HTTP API zone below
    archive: {
      storage: {
        apiKey: process.env.BUNNY_ARCHIVE_STORAGE_API_KEY,
        hostname: 'archive.b-cdn.net',
        zoneName: 'archive-zone',
        clientUploads: {
          // same script + secret as the global zone
          edge: { scriptUrl: 'https://my-uploader.b-cdn.net', secret: process.env.BUNNY_EDGE_UPLOAD_SECRET },
        },
      },
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'main-zone',
    clientUploads: {
      edge: { scriptUrl: 'https://my-uploader.b-cdn.net', secret: process.env.BUNNY_EDGE_UPLOAD_SECRET },
    },
  },
})
```

Re-run `npx payload bunny:deploy-edge-script` whenever you add or remove a non-s3 zone, so the script's zone map stays in sync. To deploy the production script from a dev machine, select the environment explicitly — e.g. `npx payload bunny:deploy-edge-script --env-file .env.production`. See [Deploy the Edge Script](/cli/deploy-edge-script).

## Multiple collections

### Per-collection zones and libraries

Point individual collections at their own Bunny storage zone and stream library. A collection whose `storage`/`stream` includes `apiKey` uses that config as its own zone/library, ignoring the global one. All of `apiKey`/`hostname`/`zoneName` (storage) or `apiKey`/`hostname`/`libraryId` (stream) are then required, and nothing — including `tokenSecurityKey`, `mimeTypes`, or `tus` — is inherited from the global config.

```ts theme={null}
bunnyStorage({
  collections: {
    // Uses the global zone + library
    media: true,
    // Uses its own storage zone and stream library
    tenantA: {
      storage: {
        apiKey: process.env.TENANT_A_STORAGE_API_KEY,
        hostname: 'tenant-a.b-cdn.net',
        zoneName: 'tenant-a-zone',
        tokenSecurityKey: process.env.TENANT_A_STORAGE_TOKEN_KEY,
      },
      stream: {
        apiKey: process.env.TENANT_A_STREAM_API_KEY,
        hostname: 'vz-tenant-a-123.b-cdn.net',
        libraryId: 654321,
        mp4Fallback: true,
        tokenSecurityKey: process.env.TENANT_A_STREAM_TOKEN_KEY, // required: signedUrls on this library
        webhook: { secret: process.env.TENANT_A_WEBHOOK_SECRET },
      },
      signedUrls: { expiresIn: 3600 },
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'main-zone',
  },
  stream: {
    apiKey: process.env.BUNNY_STREAM_API_KEY,
    hostname: 'vz-abc123def-456.b-cdn.net',
    libraryId: 123456,
    mp4Fallback: true, // media keeps Payload access control, so Stream needs this
    tus: true,
  },
})
```

See [Collection overrides](/configuration/collection-overrides#own-zone-or-library-per-collection) and [Multi-tenant](/guides/multi-tenant).

### Disable storage or stream per collection

```ts theme={null}
bunnyStorage({
  collections: {
    // Only Bunny Storage — videos won't be uploaded to Stream
    images: {
      prefix: 'images',
      stream: false,
    },
    // Only Bunny Stream — non-video files are rejected
    videos: {
      prefix: 'videos',
      storage: false,
      disablePayloadAccessControl: true,
    },
    // Both, using global defaults
    media: {
      prefix: 'mixed-content',
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'example.b-cdn.net',
    zoneName: 'my-zone',
  },
  stream: {
    apiKey: process.env.BUNNY_STREAM_API_KEY,
    hostname: 'vz-abc123def-456.b-cdn.net',
    libraryId: 123456,
    mp4Fallback: true, // media keeps Payload access control, so Stream needs this
    tus: true,
  },
})
```

See [Collection overrides](/configuration/collection-overrides).

### Different settings per collection

```ts theme={null}
bunnyStorage({
  accountApiKey: process.env.BUNNY_ACCOUNT_API_KEY,
  collections: {
    media: {
      prefix: 'public',
      disablePayloadAccessControl: true,
      thumbnail: {
        appendTimestamp: true,
        queryParams: { width: '200', height: '200' },
      },
    },
    documents: {
      prefix: 'private',
      signedUrls: {
        expiresIn: 1800,
        staticHandler: { useRedirect: false }, // proxy through Payload
      },
      purge: false,
    },
    videos: {
      prefix: 'video-content',
      disablePayloadAccessControl: true,
      stream: {
        thumbnailTime: 3000,
        tus: { expiresIn: 7200 },
      },
    },
    avatars: {
      prefix: 'avatars',
      urlTransform: {
        queryParams: { width: '150', height: '150', quality: '90' },
      },
      purge: { async: true },
    },
  },
  storage: {
    apiKey: process.env.BUNNY_STORAGE_API_KEY,
    hostname: 'cdn.example.com',
    region: 'ny',
    zoneName: 'main-storage',
    tokenSecurityKey: process.env.BUNNY_STORAGE_TOKEN_SECURITY_KEY,
  },
  stream: {
    apiKey: process.env.BUNNY_STREAM_API_KEY,
    hostname: 'video.example.com',
    libraryId: 123456,
    mp4Fallback: true,
    tokenSecurityKey: process.env.BUNNY_STREAM_TOKEN_SECURITY_KEY, // documents inherits stream + enables signedUrls
    tus: true,
    cleanup: { maxAge: 172800 },
  },
  purge: true,
  thumbnail: { appendTimestamp: true },
})
```

## Working with stored data

### Querying by bunnyData

```ts theme={null}
const results = await payload.find({
  collection: 'media',
  where: {
    'bunnyData.stream.videoId': { equals: 'e7f2c1a0-...' },
  },
})
```

See [Stored data](/guides/stored-data) for the full shape and what is stored vs. computed.

## Environment variables

```bash .env theme={null}
# Bunny Storage
BUNNY_STORAGE_API_KEY=your-storage-api-key
BUNNY_STORAGE_TOKEN_SECURITY_KEY=your-storage-token-security-key

# Bunny Stream (optional)
BUNNY_STREAM_API_KEY=your-stream-api-key
BUNNY_STREAM_TOKEN_SECURITY_KEY=your-stream-token-security-key

# Bunny account API key (cache purging + edge-script deploy)
BUNNY_ACCOUNT_API_KEY=your-bunny-account-api-key

# Client uploads (edge mode, optional)
BUNNY_EDGE_UPLOAD_SECRET=your-edge-shared-secret
```

See [Getting your credentials](/configuration/storage/overview#getting-your-credentials) and [Cache purging](/configuration/cache-purge#requirements) for where each key comes from.

## Next steps

<Columns cols={2}>
  <Card title="Multi-tenant" icon="users" href="/guides/multi-tenant">
    Per-tenant prefixes, per-tenant zones, and isolation with signed URLs.
  </Card>

  <Card title="Stored data" icon="database" href="/guides/stored-data">
    The `bunnyData` field shape and querying videos by `videoId`.
  </Card>

  <Card title="Stream collections" icon="folder-tree" href="/guides/stream-collections">
    Organize videos into per-tenant Bunny Stream collections.
  </Card>

  <Card title="Media preview" icon="circle-play" href="/guides/media-preview">
    Inline Stream video and audio previews in the admin panel.
  </Card>
</Columns>
