Storage
Uploads, MIME guards & a built-in CDN
The storage block gives your API file handling out of the box: multipart uploads with size and MIME validation, and a built-in CDN that serves the stored files back with caching, range requests and ETags. No S3 wiring required to get started.
Uploaded files are tracked as records (so they participate in auth and relations), and the CDN layer streams them efficiently — including partial content for video and large downloads.
Upload limits & guards#
Define what may be uploaded and how large it can be. MIME allow/block lists are your first line of defence against unwanted file types.
1{2 "storage": {3 "enabled": true,4 "provider": "local",5 "basePath": "./uploads",6 "maxFileSizeBytes": 10485760,7 "allowedMimeTypes": ["image/jpeg", "image/png", "image/webp", "application/pdf"],8 "cdn": {9 "enabled": true,10 "basePath": "/cdn",11 "cacheMaxAge": 86400,12 "enableRangeRequests": true,13 "enableEtag": true,14 "corsOrigins": ["https://app.acme.com"]15 },16 "formData": { "filesField": "files", "dataField": "data", "maxFiles": 5 }17 }18}enabledbooleanOptionalTurn on file handling: adds the files table to the schema and activates multipart upload validation + persistence on is_form_data entity routes. There are no separate storage-only routes; the CDN GET/HEAD routes are mounted only when cdn.enabled is also set.
falsebasePathstringOptionalBase directory on disk where uploaded files are written. This is a filesystem path, not an HTTP route prefix — files are served under cdn.basePath.
'./uploads'maxFileSizeBytesnumberOptionalHard upper bound on a single upload, in bytes. Oversized uploads are rejected before they're written.
104857600 (100 MB)allowedMimeTypesstring[]OptionalAllow-list of accepted MIME types. When set (length > 0), anything not on the list is rejected — the safest posture for user uploads. Left empty (the default), the allow-list is not enforced and every type is accepted except those on the blockedMimeTypes deny-list.
Example: ["image/jpeg", "image/png", "image/webp"]
[] (empty = no allow-list; all non-blocked types accepted)blockedMimeTypesstring[]OptionalDeny-list of forbidden MIME types. Defaults to a built-in deny-list of script-executing/dangerous types (executables, text/html, image/svg+xml, xml, javascript, php). Setting your own array REPLACES this default, so re-include the dangerous types you still want blocked.
Storage backend (local vs SMB)#
Where uploaded bytes actually live. The default local backend writes to basePath on the POSIX filesystem; provider: 'smb' puts them on a Windows/Samba file share instead, for a corporate file server that must stay the system of record.
1{2 "storage": {3 "enabled": true,4 "provider": "smb",5 "basePath": "./uploads",6 "smb": {7 "host": "fileserver.corp.local",8 "share": "Documents",9 "basePath": "nucleus/uploads",10 "domain": "CORP",11 "username": "svc-nucleus",12 "password": "…",13 "port": 445,14 "timeoutMs": 15000,15 "cache": { "enabled": true, "ttlSeconds": 300 }16 }17 }18}provider'local' | 'smb'OptionalSelects the storage backend. 'local' (the default) is the POSIX filesystem under basePath — nucleus's original behaviour. 'smb' routes reads and writes to an SMB2/3 file share configured under storage.smb. Every caller goes through a common StorageProvider interface, so the two are interchangeable. Resolved live (DB → env → literal): flipping the provider from the admin panel takes effect without a redeploy. If 'smb' is selected but the host/share are not set, storage degrades to local (logged) rather than taking uploads down.
'local'smbobjectOptionalSMB2/3 target used when provider: 'smb'. This is a genuine SMB client, not a Kubernetes CSI mount — no privileged container or volume is involved. host and share may instead be supplied as one Windows-style line via the panel's url slot (smb://host/share/sub/dir).
hoststringOptionalServer host or IP, e.g. fileserver.corp.local.
sharestringOptionalShare name, e.g. Documents.
basePathstringOptionalDirectory inside the share that scopes every stored path. Distinct from storage.basePath, which stays the local cache/derivative root.
share rootdomainstringOptionalWindows/AD domain for the service account.
usernamestringOptionalService-account user. Resolves through DB → env → literal, so it is panel-rotatable via the storage.smb.username slot.
passwordstringOptionalService-account password. Resolves through DB → env → literal and is panel-rotatable (storage.smb.password slot); rotating it reconnects the share rather than reusing a dead session.
portnumberOptionalSMB port.
445timeoutMsnumberOptionalPer-operation timeout in ms, so a hung share can't wedge a request.
15000cache{ enabled?; path?; ttlSeconds? }OptionalRead-through local cache so the CDN doesn't re-fetch over the network on every request. enabled defaults true (set false to always hit the share); path defaults to <storage.basePath>/.cache/smb; ttlSeconds (default 300) is how long a cached copy is trusted. Strongly recommended when the CDN range-serves media from SMB — otherwise each range request re-fetches over the network per chunk.
{ enabled: true, path: '<storage.basePath>/.cache/smb', ttlSeconds: 300 }CDN delivery#
Serve stored files back to clients with proper HTTP caching semantics. Range requests enable seekable media and resumable downloads; ETags enable conditional requests.
cdnobjectOptionalBuilt-in content-delivery configuration.
enabledbooleanOptionalServe files over the CDN routes. The default of true applies only inside the merged CDN config; the mount guard reads the RAW storage.cdn.enabled before the default is applied, so the CDN routes mount only when BOTH storage.enabled and storage.cdn.enabled are explicitly truthy in your config. Supplying cdn: {} with enabled omitted leaves the raw value undefined and the CDN routes UNMOUNTED despite the documented default — always set cdn.enabled: true explicitly (as the sample does). User-uploaded dangerous types are always served as downloads (Content-Disposition: attachment) with X-Content-Type-Options: nosniff — a stored-XSS defense independent of blockedMimeTypes.
true (merged config only — see note)basePathstringOptionalPublic path files are served under (e.g. /cdn).
'/cdn'cacheMaxAgenumberOptionalCache-Control max-age in seconds sent with served files.
86400 (24h)enableRangeRequestsbooleanOptionalHonour HTTP Range — required for video scrubbing and resumable downloads.
trueenableEtagbooleanOptionalEmit ETags so browsers can revalidate cheaply with 304s.
truecorsOriginsstring[]OptionalOrigins permitted to fetch CDN assets cross-origin. Defaults to ['*'] (allow-all). Access-Control-Allow-Origin emits '*' only when the first entry is '*'; otherwise it emits a comma-joined list (not spec-valid) — set a single explicit origin per deployment.
['*']transform{ enabled?; maxWidth?; maxHeight?; defaultQuality?; allowedFormats?; cacheSubdir?; maxInputPixels?; pregenerate? }OptionalOpt-in on-the-fly IMAGE resize/compress at the serve endpoint (needs the optional `sharp` peer dep — see the callout). enabled defaults false. maxWidth/maxHeight (default 3840) clamp the requested ?w/?h; defaultQuality (default 80) is the encode quality when ?q is omitted; allowedFormats (default ['webp','avif','jpeg','png']) restricts ?format; cacheSubdir (default '.cache/cdn', under storage.basePath) is where derivatives are cached; maxInputPixels (default 100000000 = 100MP) is sharp's decompression-bomb guard. pregenerate: { enabled, widths[], formats[], quality } — on upload, fire-and-forget pre-render of the listed widths×formats into the same cache so the first request is also fast.
{ enabled: false }video{ enabled?; transcode?; poster?; posterFormat?; crf?; maxWidth?; ffmpegPath? }OptionalOpt-in VIDEO optimization on upload (needs an ffmpeg binary — see the callout). enabled defaults false. On upload the CDN fires a fire-and-forget ffmpeg job: transcode (default true) → web-optimized MP4 (H.264 + +faststart, downscaled to maxWidth default 1920, quality crf default 23); poster (default true) → a poster frame (posterFormat 'jpeg'|'webp', default 'jpeg') served at ?poster=1. ffmpegPath (default 'ffmpeg') is the binary name/path. Derivatives cache beside the image ones (transform.cacheSubdir).
{ enabled: false }Form-data field mapping#
Multipart uploads carry the binary files in one field and a JSON metadata blob in another. These settings name those fields and cap how many files one request may carry.
1// multipart upload to an is_form_data entity route (e.g. the built-in `files` table);2// fields match formData config. Do NOT send file metadata — it's server-derived.3const form = new FormData();4form.append("files", file); // filesField5form.append("data", JSON.stringify({})); // dataField — your own entity columns only6 7await fetch("/files", { method: "POST", body: form });8// a file persisted in the `files` table is then served back at <cdn.basePath>/<id>formDataobjectOptionalMultipart request shape.
filesFieldstringOptionalThe form field carrying the binary file(s). Defaults to "files" when the field is unset.
'files'dataFieldstringOptionalThe form field carrying the JSON metadata. Defaults to "data" when the field is unset.
'data'maxFilesnumberOptionalMaximum number of files stored from a single multipart request. Extra files beyond this cap are silently ignored — only the first maxFiles are uploaded and the request is not rejected.
10Under the hood — StorageProvider & BunFileManager#
Storage sits behind a pluggable StorageProvider interface — LocalStorageProvider (POSIX filesystem), SmbStorageProvider (SMB2/3 share) and a LiveStorageProvider that picks the backend per call from the panel's setting, plus a migrateStorage() helper to copy bytes between backends. The LOCAL provider (and the CDN's own file metadata reads) are backed by BunFileManager, built on Bun's native file APIs (Bun.file / Bun.write) — the crash-safe primitive described below. The typed reads, streaming and atomic-write guarantees here describe the local backend specifically; SMB bytes are read through the read-through cache.
typed readstext · json · buffer · bytes · streamOptionalreadFile returns the format you ask for, and getFileInfo surfaces size, MIME type, extension and timestamps — the CDN uses its size and modified time for Content-Length/Last-Modified/ETag. (Content-Type is served from the stored file record's mime_type, and upload validation checks the client-declared MIME, not getFileInfo.)
streaminglarge filesOptionalwriteStream / readFileStream / copyFileStream move data in chunks rather than buffering whole files in memory, so large uploads and downloads (and CDN range responses) stay flat on memory.
atomic writescrash-safeOptionalatomicWrite / atomicJsonWrite write to a temp file and swap, safeFileUpdate wraps a read-modify-write with automatic rollback, and batchAtomicOperations groups several — so a crash mid-write never leaves a half-written file.
permissionsoctal + helpersOptionalsetPermissions and the makeReadable/Writable/ReadOnly helpers manage POSIX file modes, letting stored artifacts be locked down on disk.
Related sections