storage-bunny
| Package | @theoven/storage-bunny |
| Adds to context | ctx.storage — it is a driver for storage |
| Endpoints | none |
| Creates files | whatever you upload, in your storage zone |
| Creates tables | none |
| Status | shipped |
Install
Section titled “Install”bun add @theoven/storage @theoven/storage-bunnyimport { storage } from '@theoven/storage'import { bunnyStorage } from '@theoven/storage-bunny'
export const app = createApp().use( storage( bunnyStorage({ zone: env.string('BUNNY_ZONE'), accessKey: env.string('BUNNY_ACCESS_KEY'), pullZone: 'cdn.example.com', }), ),)What it does
Section titled “What it does”Puts Bunny.net Storage behind the storage contract. Everything on ctx.storage works exactly as
it does on S3 or disk — the driver is the only line that changes. What Bunny adds is cheap egress
through its pull zones, which is the reason to pick it.
Endpoints
Section titled “Endpoints”This brick adds no endpoints.
Uploading is ctx.storage.upload() — the same call as every other driver:
import { z } from 'zod'
export const body = z.object({ file: z.file() })
export default async ({ body, storage }) => { const object = await storage.upload(`uploads/${body.file.name}`, body.file) return { key: object.key, size: object.size }}upload takes anything web-shaped — a File, Blob, ArrayBuffer, ReadableStream or string —
and returns { key, bucket, size, type, lastModified }.
await storage.upload('reports/q3.pdf', pdfBytes, { type: 'application/pdf' })
const blob = storage.download('reports/q3.pdf') // lazy — no request until readconst text = await blob.text()
await storage.exists('reports/q3.pdf')await storage.stat('reports/q3.pdf')await storage.list({ prefix: 'reports/', limit: 50 })await storage.delete('reports/q3.pdf')Full detail on each of these is on the storage page; they behave the same here.
What it creates
Section titled “What it creates”Nothing in your repository. Objects go to the storage zone you configured.
Reads go through the edge
Section titled “Reads go through the edge”With pullZone set, download() fetches from the CDN. Without it, reads fall back to the storage
API — which works, and is slower and costs more.
The zone password is never sent to the pull zone host; a test asserts that, because leaking a storage credential to a public edge is not a mistake you want to make once.
Configuration
Section titled “Configuration”| Option | Default | |
|---|---|---|
zone |
— | required; the storage zone name |
accessKey |
— | required; the zone password |
region |
storage.bunnycdn.com |
ny., la., sg., syd., uk., se., br., jh. |
pullZone |
— | hostname for public URLs |
tokenKey |
— | token-authentication key, if enabled on the pull zone |
A zone created in one region is not reachable through another’s host.
Signed URLs
Section titled “Signed URLs”ctx.storage.presignDownload('reports/q3.pdf', { expiresIn: 300 })Available only once both pullZone and tokenKey are set — presign is undefined otherwise,
so canPresign tells you before it matters.
Capabilities
Section titled “Capabilities”upload, download, delete, exists, stat, list |
✓ |
presignDownload |
✓, once pullZone and tokenKey are set — check storage.canPresign |
presignUpload |
✗ — throws; Bunny has no signed-upload equivalent |
directUpload |
✗ — depends on presignUpload |
Multiple buckets via storage.bucket() |
✗ — one zone per driver; register a second driver instead |
Limitations
Section titled “Limitations”- No browser-direct uploads — see above; server-side
upload()is unaffected. - Uploads are buffered. Bunny’s
PUTneeds a known length, so a stream is collected before sending. S3 streams; this does not. listis directory-based. A prefix is split into a directory and a “starts with”, so a prefix spanning directories does not work the way it does on S3.- Paging is client-side. Bunny returns a whole directory;
limitandafterslice it here.
How it is verified
Section titled “How it is verified”Request shapes, key encoding, pull-zone routing, paging and the URL signing are tested against an
injected fetcher — deterministic, and where the bugs are. The token is checked across forty keys
to be URL-safe, since base64’s + and / do not survive a query string.
That Bunny accepts those requests is not verified here: live tests are gated on BUNNY_ZONE
and skip with a printed notice.