Skip to content
BunBase BunBase BunBase Docs Alpha v0.1.0

Storage

BunBase includes a built-in file storage system with support for local filesystem and S3-compatible providers.

ProviderUse case
localDevelopment and simple self-hosted deployments
s3Production, CDN-backed, multi-region

Set STORAGE_PROVIDER=s3 and configure S3 credentials to switch providers. See Configuration.

Files are organized into buckets. Each bucket has its own access policy and optional constraints.

POST /api/v1/admin/buckets
Authorization: Bearer <admin-secret>
Content-Type: application/json
{
"name": "avatars",
"is_public": true,
"allowed_mime_types": ["image/jpeg", "image/png", "image/webp"],
"max_size_bytes": 5242880
}

The default bucket is created automatically with private access.

By default, uploading to a bucket that does not exist returns 404. You can enable automatic bucket creation in Studio › Settings:

  • When Auto-create buckets is on, any named bucket that does not exist is created on first upload with authenticated read/write access and no MIME or size restrictions.
  • The bucket is created once and reused for subsequent uploads.
POST /api/v1/storage/upload
Authorization: Bearer <access-token>
Content-Type: multipart/form-data
file=<binary>
bucket=avatars
is_public=true

Response 201:

{
"id": "01JKX...",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"bucket": "avatars",
"url": "/api/v1/storage/01JKX...",
"is_public": true
}
GET /api/v1/storage/:id

Public files are accessible without authentication. Private files require a valid Authorization header or a signed URL.

DELETE /api/v1/storage/:id
Authorization: Bearer <access-token>

Only the file owner or an admin can delete a file.

Generate a time-limited URL for private file access or direct-upload:

POST /api/v1/storage/sign
Authorization: Bearer <access-token>
Content-Type: application/json
{
"filename": "photo.jpg",
"content_type": "image/jpeg",
"bucket": "avatars",
"is_public": true,
"expires_in": 3600
}

Returns:

{ "key": "avatars/01JKX....jpg", "url": "<presigned-put-url>", "expires_in": 3600 }

The returned url is provider-dependent:

  • Local provider: points at PUT /api/v1/storage/:key?token=... — PUT the file body directly and the server registers metadata in one step.
  • S3 provider: points directly at your S3 bucket — PUT the file body to S3, then call POST /api/v1/storage/confirm to register metadata with BunBase.

After a successful S3 presigned PUT:

POST /api/v1/storage/confirm
Authorization: Bearer <access-token>
Content-Type: application/json
{
"key": "avatars/01JKX....jpg",
"bucket": "avatars",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"size": 204800,
"is_public": true
}

Returns a FileRecord (201). BunBase verifies the object exists in storage before registering.

When uploading, set collection and record_id to link a file to a record:

POST /api/v1/storage/upload
Authorization: Bearer <access-token>
file=<binary>
collection=posts
record_id=01JKX...
// Upload
const file = await client.storage.upload(blob, {
bucket: "avatars",
isPublic: true,
});
// Download URL
const url = client.storage.downloadUrl(file.id, file.filename ?? undefined);
// Delete
await client.storage.delete(file.id);

All uploads — multipart form uploads (POST /storage/upload), presigned PUT via the local provider, and admin uploads — stream file data directly to storage without buffering the entire body in memory.

For large files this means constant memory usage regardless of file size. The streaming path is automatic; no changes are needed in your client code.

If the actual uploaded size exceeds the bucket’s max_size_bytes (even if Content-Length was below the limit), the file is deleted and a 413 is returned.

If a bucket has allowed_mime_types set, uploads with a non-matching MIME type are rejected with 415 Unsupported Media Type.

If a bucket has max_size_bytes set, uploads exceeding the limit are rejected with 413 Request Entity Too Large.

BunBase can resize, crop, and convert images on the fly. Pass query parameters on any image download URL to apply a transform. Only image MIME types are transformed (image/jpeg, image/png, image/webp, image/gif, image/avif). Non-image files are served as-is regardless of params.

Transformed variants are cached on disk after the first request. Subsequent requests for the same transform serve the cached file immediately.

ParameterDescriptionExample
wOutput width in pixels (maintains aspect ratio)?w=400
hOutput height in pixels (maintains aspect ratio)?h=300
w + hResize to fit within the box (maintains aspect ratio, no crop)?w=400&h=300
fit=coverResize and center-crop to exactly w×h?w=400&h=300&fit=cover
fit=containResize to fit within w×h with white letterboxing?w=400&h=300&fit=contain
formatConvert output format: webp, jpeg, or png?format=webp
qJPEG/WebP quality, 1–100 (default 85)?q=75

Maximum output dimension on either side is 4096 px (clamped silently).

# Resize to fit within 800×600, keep original format
GET /api/v1/storage/01JKX...?w=800&h=600
# Thumbnail: crop to exactly 200×200, convert to WebP at quality 80
GET /api/v1/storage/01JKX...?w=200&h=200&fit=cover&format=webp&q=80
# Convert to WebP only (no resize)
GET /api/v1/storage/01JKX...?format=webp
# Resize width only, keep aspect ratio
GET /api/v1/storage/01JKX...?w=400

You can configure a CDN URL per bucket in Studio › Settings › Storage or via the Admin API. When a bucket has a cdn_url set, download requests for files in that bucket are redirected to the CDN instead of being served through BunBase:

GET /api/v1/storage/:id
→ 302 Location: https://cdn.example.com/avatars/01JKX....jpg

This applies to both local and S3 providers. Image transforms (?w=, ?h=, etc.) always go through BunBase even when a CDN is configured, since the transform pipeline needs to read the source bytes.

CDN setup (S3/R2): point your CDN at your S3 bucket, set the bucket’s cdn_url in BunBase, and files are served directly from the CDN edge.

CDN setup (local): point your CDN at {PUBLIC_URL}/api/v1/storage/. Public files can be cached at the CDN edge indefinitely (Cache-Control: public, max-age=31536000, immutable). Private files are served with Cache-Control: private so the CDN will not cache them.

See Configuration for the storage_cdn_url global fallback setting.