Storage and Uploads
timbl stores uploaded files through a StorageAdapter. The default writes to a local directory and serves files through a reverse proxy or the storage adapter’s url method. This page covers the default and how to replace it.
The default storage adapter
The default adapter writes files to a local directory on disk and returns a URL pointing to where your reverse proxy or static file server can serve them. It implements the three StorageAdapter methods:
upload(input): writes theFileto disk, returns aStoredFilewithfilename,originalName,mimeType,size,path, andurl.delete(path): removes the file from disk.url(path): returns the public URL for a stored file.
You configure the base directory and the URL prefix through runtime options. The runtime passes upload requests through POST /api/assets (see HTTP API Reference).
Upload field
Add an upload field to any collection to attach files. Set many to true for a gallery, restrict types with allowedMimeTypes, and cap size with maxSize.
{ key: "featuredImage", type: "upload" }{ key: "gallery", type: "upload", many: true, allowedMimeTypes: ["image/png", "image/jpeg"], maxSize: 5242880 }The runtime validates the file type and size before handing the upload to the storage adapter. See Field Types for the full upload options.
Uploading via the API
Uploads use multipart/form-data. The file field is required. Optional fields: directory, filename, alt.
curl -X POST http://localhost:3000/api/assets \ -H "Cookie: ..." \ -F "file=@photo.jpg" \ -F "directory=posts"The response is the stored asset record. Reads (GET /api/assets, GET /api/assets/:id) are public. Deletes (DELETE /api/assets/:id) require a session.
Serving files
The default headless stack does not serve files from /uploads/* itself. That path is reserved for your reverse proxy or static file server. Configure your proxy (Caddy, Nginx, Traefik) to serve the storage directory at the URL prefix the adapter returns. See Deployment for reverse proxy configs that handle this.
Custom storage adapter
Replace the default with your own by implementing StorageAdapter and passing it into defineCMS.
import { defineCMS } from "timbl";import type { StorageAdapter, StorageUploadInput, StoredFile } from "@timbl/core";
const createS3StorageAdapter = (options: { bucket: string; region: string }): StorageAdapter => { return { async upload(input: StorageUploadInput): Promise<StoredFile> { // Upload to S3, return StoredFile with the public URL }, async delete(path: string): Promise<void> { // Delete from S3 }, url(path: string): string { return `https://${options.bucket}.s3.${options.region}.amazonaws.com/${path}`; }, };};
export default defineCMS({ collections: [/* ... */], adapters: { storage: createS3StorageAdapter({ bucket: "my-assets", region: "us-east-1" }), },});StoredFile requires six fields: filename, originalName, mimeType, size, path, and url. The runtime validates your implementation against StorageAdapterMethodSchemas at startup, so a missing or mis-typed method fails fast.
See Custom Adapters for the full interface and the validation contract.
See also
- Field Types: the
uploadfield and its options - Custom Adapters: all four adapter interfaces
- Deployment: reverse proxy configs for serving uploads
- HTTP API Reference: the asset upload and read endpoints