Files API
Upload, serve, thumbnail, and protect files.
Upload
Section titled “Upload”POST /api/v1/files/<col>/<recordId>/<field>Content-Type: multipart/form-data
file: <binary>file: <binary> ← repeat for multi-file fieldsThe 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— nofileform field, single-file field with multiple uploads, or the field isn’t a file field401— not signed in403— the collection’supdate_rulerefuses the caller404— collection, record or field not found413— over the max upload size415— 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=200x200GET /api/v1/files/<filename>?thumb=400x300&fit=coverGET /api/v1/files/<filename>?thumb=400x300_cover ← shorthandGET /api/v1/files/<filename>?thumb=800x0&format=auto&quality=75GET /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 asapplication/octet-streamwithContent-Disposition: attachment, so the browser downloads it instead of rendering it on your API’s origin.Content-Security-Policy: sandbox; default-src 'none'andX-Content-Type-Options: nosniffon 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:
| Param | Notes |
|---|---|
thumb | WIDTHxHEIGHT, 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. |
fit | contain (default) · cover · crop (alias of cover). cover center-crops the source to the target aspect, then resizes to exactly WxH. Cache key includes mode. |
format | webp · jpeg · png · auto. Re-encode; auto picks WebP when Accept allows it. Works without thumb. Invalid value → 400. |
quality | 1–100, JPEG / WebP output. Invalid value → 400. |
token | Required when the file’s field has protected: true. |
Issue a protected-file token
Section titled “Issue a protected-file token”POST /api/v1/files/<col>/<recordId>/<field>/<filename>/tokenAuthorization: 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.
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 } }Delete
Section titled “Delete”# All files for a record's fieldDELETE /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.
Storage layout
Section titled “Storage layout”<dataDir>/uploads/ <uuid>.png ← uploaded files <uuid>.pdf .thumbs/ ← generated thumbnail cache <uuid>.png__200x200 <uuid>.png__400x300_coverFiles 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.
Field options that affect files
Section titled “Field options that affect files”{ "name": "avatar", "type": "file", "options": { "maxSize": 5242880, "mimeTypes": ["image/*"], "multiple": false, "protected": false} }| Option | Effect |
|---|---|
maxSize | Bytes, enforced at upload time. 0 or unset = only the global max upload size applies. |
mimeTypes | Patterns; "image/*" matches any image/* MIME. Empty = any. |
multiple | Field stores a JSON array of filenames. |
protected | GET requires ?token=<jwt> from the token-issue endpoint. |