Skip to content

Files API

Upload, serve, thumbnail, and protect files.

POST /api/v1/files/<col>/<recordId>/<field>
Content-Type: multipart/form-data
file: <binary>
file: <binary> ← repeat for multi-file fields

The record must already exist. Uploading needs a signed-in caller (anonymous requests get 401), and non-admins must pass the collection’s update_rule. Validates maxSize and mimeTypes per the field options before writing. For multi-file fields, repeat the file form key to upload several at once.

The upload only stores the file. Set the field to the returned filename (a record update) to attach it; a file no record names is removed by the hourly cleanup once it’s a day old.

Every upload request is also capped by the max upload size setting (files.max_upload_size, default 50 MB, range 1 KB–128 MB; Settings → File storage). Requests over it get 413, checked from Content-Length before the body is read. A field’s own maxSize applies on top.

Single-file response:

{
"data": {
"id": "<uuid>",
"filename": "<uuid>.png",
"originalName": "avatar.png",
"size": 12345,
"mimeType": "image/png"
}
}

Multi-file response: same shape under data: [...].

Errors:

  • 400 — no file form field, single-file field with multiple uploads, or the field isn’t a file field
  • 401 — not signed in
  • 403 — the collection’s update_rule refuses the caller
  • 404 — collection, record or field not found
  • 413 — over the max upload size
  • 415 — MIME type outside the server’s allow-list (e.g. text/html)
  • 422 — file too large or MIME type rejected by the field (details: { fieldName: hint })
GET /api/v1/files/<filename>
GET /api/v1/files/<filename>?thumb=200x200
GET /api/v1/files/<filename>?thumb=400x300&fit=cover
GET /api/v1/files/<filename>?thumb=400x300_cover ← shorthand
GET /api/v1/files/<filename>?thumb=800x0&format=auto&quality=75
GET /api/v1/files/<filename>?token=<jwt>

Returns the binary stream. Headers:

  • Content-Type — the MIME recorded at upload, only for safe raster images (PNG, JPEG, GIF, WebP, AVIF). Everything else — SVG, PDF, HTML, text — is served as application/octet-stream with Content-Disposition: attachment, so the browser downloads it instead of rendering it on your API’s origin.
  • Content-Security-Policy: sandbox; default-src 'none' and X-Content-Type-Options: nosniff on every file response.
  • For thumbnails the format is preserved end-to-end: PNG → image/png, JPEG → image/jpeg, GIF → image/gif (single-frame GIFs come out as PNG), WebP → image/webp. AVIF files are not resized; ?thumb= returns the original.

The upload’s type is read from the multipart part (Bun derives it from the file name’s extension when it knows it), and MIME parameters such as ;charset=utf-8 are ignored. A type outside the allow-list, such as .html, is refused with 415. Stored file names take their extension from the validated MIME type, not from the name the client uploaded; unknown types are stored as .bin.

Query params:

ParamNotes
thumbWIDTHxHEIGHT, 1–4096 each axis; one side may be 0 to keep the aspect ratio. PNG / JPEG / GIF (animated frames preserved) / WebP. AVIF and non-images served unchanged. Optional _<mode> suffix selects the fit mode. If the field lists allowed thumbs, other sizes return the original (X-Vaultbase-Thumb: not-allowed) for non-admins.
fitcontain (default) · cover · crop (alias of cover). cover center-crops the source to the target aspect, then resizes to exactly WxH. Cache key includes mode.
formatwebp · jpeg · png · auto. Re-encode; auto picks WebP when Accept allows it. Works without thumb. Invalid value → 400.
quality1–100, JPEG / WebP output. Invalid value → 400.
tokenRequired when the file’s field has protected: true.
POST /api/v1/files/<col>/<recordId>/<field>/<filename>/token
Authorization: Bearer <admin-or-user-jwt>

Returns:

{ "data": { "token": "<jwt>", "expires_at": 1730003600, "one_time": false } }

1-hour TTL. The token is a JWT with audience: "file" and a filename claim; the claim is checked at GET time — a token issued for a.png cannot unlock b.png. one_time is true when the field has oneTimeToken set: the token works for one download (a second answers 410), so don’t cache it.

Who can issue a token: admins always pass. Other callers pass iff the collection’s view_rule (and the field’s viewRule) would let them read the parent record: null → anyone, including unauthenticated callers, "" → admin only, expression → evaluated.

Terminal window
curl -X POST \
-H "Authorization: Bearer $JWT" \
https://api.example.com/api/v1/files/posts/$ID/cover/$FN/token
# → { "data": { "token": "<jwt>", "expires_at": 1730003600, "one_time": false } }
# All files for a record's field
DELETE /api/v1/files/<col>/<recordId>/<field>
# A specific file (multi-file fields)
DELETE /api/v1/files/<col>/<recordId>/<field>/<filename>

Returns { data: { deleted: <count> } } or { data: null }. Sweeps cached thumbnails for the deleted file as a side effect. The record isn’t changed: its field still names the deleted file until you update it.

<dataDir>/uploads/
<uuid>.png ← uploaded files
<uuid>.pdf
.thumbs/ ← generated thumbnail cache
<uuid>.png__200x200
<uuid>.png__400x300_cover

Files that no record names any more (the record or collection was deleted, or the field was changed) are removed by the hourly cleanup after 24 hours; the DELETE endpoints remove a file immediately. Thumb cache is auto-invalidated on file delete.

{ "name": "avatar", "type": "file", "options": {
"maxSize": 5242880,
"mimeTypes": ["image/*"],
"multiple": false,
"protected": false
} }
OptionEffect
maxSizeBytes, enforced at upload time. 0 or unset = only the global max upload size applies.
mimeTypesPatterns; "image/*" matches any image/* MIME. Empty = any.
multipleField stores a JSON array of filenames.
protectedGET requires ?token=<jwt> from the token-issue endpoint.