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.

yatta/func/storage.tsts
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

Uploadsts
// 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

Downloadsts
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.

yatta/backend/media/[name].tsts
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

Time-limited linksts
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

Browsingts
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"); // boolean

Moving and deleting

Mutationsts
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

Scoped directoriests
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.

Built-in routests
GET /storage              → explorer UIGET /storage/api/files     → JSON listingGET /storage/files/<key>   → file contents

Sizes and durations

tsts
// Sizes"500B"  "10KB"  "5MB"  "1GB"   // strings10485760                              // raw bytes // Durations"30s"  "5m"  "1h"  "7d"