Skip to content

Storage

SemitraStorage is the storage subsystem in Semitra.

Use it for object files, attachment bodies, and reusable binary assets.

Semitra storage supports:

  • object put/get/delete
  • text and URL helpers
  • tenant-aware prefixes
  • attachment helpers
  • attachment metadata persistence
  • optional base URL resolution

Attachments are a storage feature tied to records.

That gives you a clean path for common application workflows:

  • attach a cover image to a post
  • attach a generated PDF to a report record
  • replace an old uploaded file when a new one arrives
  • keep attachment metadata aligned with the owning record

The storage API works the same way for general object writes:

import { SemitraStorage } from "@semitra/cli";
const storage = SemitraStorage.fromR2(env.ASSETS, {
baseUrl: "https://cdn.example.com",
keyPrefix: "uploads",
scope: "tenant"
});
await storage.put("reports/monthly.pdf", pdfBytes, {
contentType: "application/pdf"
});
const url = await storage.url("reports/monthly.pdf");

Use that path for binary objects that belong in R2, not D1: uploads, exports, avatars, receipts, or generated documents.

For record-owned files, keep the attachment close to the model that owns it:

const post = await Posts.find(String(this.params.get("id")));
await post.attachment("cover").attach(imageBytes, {
filename: "cover.png",
contentType: "image/png"
});
const coverUrl = await post.attachment("cover").url();

That is the better fit when the file belongs to a single record and you want Semitra to keep attachment metadata aligned with that record automatically.

  • user avatars
  • document uploads
  • generated exports
  • media assets
  • record-attached files such as a cover image or receipt

Storage should store objects, not become a general application database.

If the work is metadata-heavy or relational, keep the metadata in D1 and let storage own only the object body and object location.