Engines
Storage
Uploads and downloads through one interface, whether the bytes live on local disk or in S3. Includes a built-in explorer UI, signed URLs, and correct range streaming.
Disks
Configure one or more named disks. The default is used when a disk is not named.
import { createStorage } from "yatta/storage"; declare module "yatta/storage" { interface StorageRegister { disks: "local" | "s3"; }} export const storage = createStorage({ default: "local", disks: { local: { driver: "local", baseDir: "./storage/uploads", publicUrl: "/storage/files", }, s3: { driver: "s3", bucket: process.env.S3_BUCKET || "my-bucket", endpoint: process.env.S3_ENDPOINT, // R2 / MinIO accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }, },});Warning
Local disk is per-machine. Behind more than one node, point the default disk at
s3 — otherwise each node serves different bytes.Uploading
// Simpleawait storage.disk("local").upload("avatars/1.png", buffer, { contentType: "image/png",}); // Fluent, with validationawait storage .file("reports/2026-q1.pdf") .from(buffer) .maxSize("10MB") .verifyMagic() // inspect actual bytes, not the declared type .hashName() // content-addressed filename .save(); // From a web uploadawait storage.disk("local").upload("uploads/x.png", request);Reading
const file = await storage.disk("local").download("avatars/1.png"); await file.text();await file.json();await file.buffer();await file.arrayBuffer();await file.blob(); for await (const chunk of file.stream()) { // …}Serving over HTTP
serve implements RFC 9110 properly: byte ranges produce 206, conditional requests produce 304, and unsatisfiable ranges produce 416.
import { API, createAPI } from "yatta/api";import { storage } from "../func/storage"; const api = createAPI(); api.get(async (ctx) => { return storage.file(`media/${ctx.params.name}`).serve(ctx.req);}); export default api;Dragging the seek bar on a large video then resumes instead of re-downloading, because the range request is honoured.
Signed URLs
const url = await storage.disk("local").signedUrl("reports/q1.pdf", { expiresIn: "2h",}); // Fluentconst url = await storage .file("reports/q1.pdf") .sign() .expiresIn("2h") .get();Note
Signed URLs are signed with
STORAGE_SECRET. Set it in production — an ephemeral secret makes every link invalid after a restart.Listing and metadata
await storage.disk("local").list();await storage.disk("local").list({ prefix: "avatars/" }); const meta = await storage.disk("local").head("avatars/1.png");// { size, contentType, lastModified, etag } await storage.disk("local").exists("avatars/1.png"); // booleanMoving and deleting
await storage.disk("local").copy("a.png", "b.png");await storage.disk("local").move("a.png", "archive/a.png");await storage.disk("local").delete("a.png");Folders
const folder = storage.folder("uploads"); await folder.put("file.txt", "hello");await folder.list();await folder.delete("file.txt"); await folder.file("nested/deep.bin").put(buffer);Explorer UI
/storage serves a browsable dashboard for the default disk, plus a JSON listing endpoint — no extra configuration.
GET /storage → explorer UIGET /storage/api/files → JSON listingGET /storage/files/<key> → file contentsSizes and durations
// Sizes"500B" "10KB" "5MB" "1GB" // strings10485760 // raw bytes // Durations"30s" "5m" "1h" "7d"