Files
Files attach to records via fields of type file. By default they live on
the local filesystem at <dataDir>/uploads/; switch to S3 / Cloudflare R2
from Settings → File storage — see Storage.
Schema
Section titled “Schema”{ "name": "avatar", "type": "file", "options": { "maxSize": 5242880, // 5 MB "mimeTypes": ["image/*"], // patterns; "image/*" supported "multiple": false, "protected": false} }The record stores the filename (or a JSON array of filenames if multiple).
The actual file lives at <dataDir>/uploads/<id>.<ext>.
Upload
Section titled “Upload”POST /api/v1/files/<collection>/<recordId>/<field>Content-Type: multipart/form-datafile: <binary>Server validates maxSize and mimeTypes before writing anything. Single-file
fields reject more than one upload; multi-file fields accept any count.
Response:
{ "data": { "id": "...", "filename": "...uuid....png", "originalName": "avatar.png", "size": 12345, "mimeType": "image/png" }}For multi-file uploads, data is an array.
GET /api/v1/files/<filename>Public by default; binary stream with the original Content-Type.
Thumbnails
Section titled “Thumbnails”Add ?thumb=WIDTHxHEIGHT to the GET — Vaultbase generates a thumbnail,
caches it on disk, and serves the cache on subsequent hits.
GET /api/v1/files/<filename>?thumb=200x200GET /api/v1/files/<filename>?thumb=64x64&fit=coverGET /api/v1/files/<filename>?thumb=400x300_crop ← shorthandFit modes
Section titled “Fit modes”| Mode | Behavior |
|---|---|
contain (default) | Fit-within: preserve aspect ratio, the longest axis matches W or H. Output may be smaller than the requested box. |
cover | Center-crop the source to the target aspect ratio, then resize to exactly WxH. No letterboxing, no distortion. |
crop | Alias for cover. |
Two ways to pick a mode:
GET /api/v1/files/<filename>?thumb=400x300&fit=coverGET /api/v1/files/<filename>?thumb=400x300_cover ← shorthand suffixThe cache key includes the mode, so contain and cover thumbnails for
the same file co-exist without colliding.
- Supported source formats: PNG, JPEG, GIF (animated → animated thumb), WebP, AVIF.
- Output: same format as input. JPEG → JPEG (q85), PNG → PNG, GIF →
GIF (frames preserved with original delays, disposal modes, and loop
count; single-frame GIFs downgraded to PNG), WebP → WebP, AVIF → AVIF.
Content-Typealways matches what’s on disk (sniffed at serve time). - Cache:
<dataDir>/uploads/.thumbs/<filename>__<W>x<H>_<mode>— invalidated on file delete. - Non-image files: served unchanged (the
?thumb=is silently ignored).
Range bounds: 1×1 to 4096×4096.
# Hero image, exact 1200x400, center-croppedcurl -o hero.jpg "https://api.example.com/api/v1/files/$FN?thumb=1200x400&fit=cover"
# Avatar, 64x64 fit-within (default)curl -o avatar.png "https://api.example.com/api/v1/files/$FN?thumb=64x64"Protected files
Section titled “Protected files”Set protected: true on the field’s options. GET /api/v1/files/<filename>
then requires ?token=<jwt>. Issue tokens via:
POST /api/v1/files/<col>/<recordId>/<field>/<filename>/tokenAuthorization: Bearer <admin-or-user-jwt>Response:
{ "data": { "token": "<jwt>", "expires_at": 1730003600 } }Then:
GET /api/v1/files/<filename>?token=<jwt>The token’s filename claim is checked against the requested path — a
token for a.png can’t unlock b.png. Tokens are 1-hour JWTs with
audience: "file".
Who can issue a token
Section titled “Who can issue a token”Admins always pass. Authenticated users pass iff the collection’s
view_rule would let them read the parent record:
view_rule | Behavior |
|---|---|
null | Public — any authenticated user can mint a token |
"" | Admin only — non-admins get 403 |
Expression (e.g. @request.auth.id = owner) | Evaluated against the record + caller. Pass → token; fail → 403. |
So the same rules that gate reading the record gate minting a file token — no new policy surface to maintain.
Admin UI auto-tokens
Section titled “Admin UI auto-tokens”The bundled admin records page handles protected file previews automatically — it mints tokens behind the scenes when rendering thumbnails and download links, so you don’t need to plumb tokens through manually when working in the UI.
Rule-based file protection
Section titled “Rule-based file protection”view_rule on the parent collection is one knob; for fine-grained control
each file field can carry its own access policy. Rules are
AND-combined — both the collection’s view rule and the field rule
must pass.
{ "name": "tax_doc", "type": "file", "options": { "viewRule": "@auth.id = record.owner", "requireAuth": true, "oneTimeToken": true, "bindTokenIp": true, "auditDownloads": true} }| Option | Behavior |
|---|---|
viewRule | Per-field rule, evaluated AND with view_rule. null/missing inherits the collection rule. "" forces admin-only. |
requireAuth | Anonymous fetches are rejected even on a public collection. |
oneTimeToken | Tokens minted from /token are single-use; second fetch returns 410 Gone. |
bindTokenIp | Token’s JWT carries the requesting client’s IP; re-use from a different IP returns 403. Incompatible with mobile NAT — opt-in. |
auditDownloads | Each successful fetch appends a files.download row to the audit log (actor, target, ip, via: rule/token). |
Extra rule context
Section titled “Extra rule context”Per-field rules see the same operands as collection rules, plus a few
file-specific headers under @request.headers.*:
| Operand | Source |
|---|---|
@request.headers.x_vb_ip | Client IP (respects trusted proxies) |
@request.headers.x_vb_file_field | The field name being downloaded |
@request.headers.x_vb_file_mime | MIME type of the stored file |
@request.headers.x_vb_file_size | Size in bytes (string) |
@request.headers.x_vb_collection | Parent collection name |
So you can write:
@auth.id = record.owner && @request.headers.x_vb_file_size < '5000000'Examples
Section titled “Examples”// Avatar visible on a public profile collection, but only the owner can// fetch the original file.{ "viewRule": "@auth.id = record.id" }
// Internal-only document — collection is public-list, file is admin-only.{ "viewRule": "" }
// Paid-tier attachment.{ "viewRule": "@auth.tier = 'paid'", "requireAuth": true }
// Sensitive download — single-use token, IP-bound, audited.{ "viewRule": "@auth.id = record.assignee", "oneTimeToken": true, "bindTokenIp": true, "auditDownloads": true }Failure modes
Section titled “Failure modes”| Status | Reason |
|---|---|
401 | Legacy protected: true field with no ?token= query parameter |
403 | Collection or field rule denied; or token IP claim mismatch |
410 | One-time token replayed |
Multi-file fields
Section titled “Multi-file fields”Set multiple: true to store an array. The record’s field is JSON-encoded:
{ "attachments": ["uuid-a.pdf", "uuid-b.pdf"] }Delete a single file from a multi-file field:
DELETE /api/v1/files/<col>/<recordId>/<field>/<filename>Delete all files for a record’s field:
DELETE /api/v1/files/<col>/<recordId>/<field>Cleanup
Section titled “Cleanup”Vaultbase doesn’t auto-delete files when their owning record is removed — this is intentional (files might be referenced from elsewhere). Call the DELETE endpoint explicitly when you want to free space.
Thumbnails are cleaned automatically when their source file is deleted.
Storage backends
Section titled “Storage backends”Two drivers ship: local (default, <dataDir>/uploads/) and s3
(AWS S3, Cloudflare R2, MinIO, B2 — anything S3-compatible, no SDK
required). Switch in the admin at Settings → File storage.