Reference / Storage

Storage

Organize files into isolated buckets, upload from the browser or Node.js, and serve thumbnails straight from the CDN.

Buckets#

Buckets are isolated containers with their own visibility (public vs. private) and usage counters. Toggling isPubliccascades to every file's ACL.

List buckets#

GET/api/storage/companies/{slug}/buckets
const buckets = await sentroy.buckets.list()

Get bucket#

GET/api/storage/companies/{slug}/buckets/{slug}
const bucket = await sentroy.buckets.get("product-assets")

Create bucket#

POST/api/storage/companies/{slug}/buckets
const created = await sentroy.buckets.create({
  name: "User Uploads",
  description: "Avatars and profile media",
  isPublic: false,
})

slug is auto-derived from name if omitted.

Update bucket#

PATCH/api/storage/companies/{slug}/buckets/{slug}
await sentroy.buckets.update("product-assets", { isPublic: true })

Delete bucket#

DELETE/api/storage/companies/{slug}/buckets/{slug}?force=true
await sentroy.buckets.delete("product-assets", { force: true })

Media#

Upload, list, download, and delete files inside a bucket. The same access token authorizes both mail and storage calls — no separate credential.

List files#

GET/api/storage/companies/{slug}/buckets/{slug}/media?type=image&limit=50
const { items, total } = await sentroy.media.list("product-assets", {
  type: "image",
  limit: 50,
})

Get media record#

GET/api/storage/companies/{slug}/buckets/{slug}/media/{id}
const media = await sentroy.media.get("product-assets", mediaId)

Upload#

Multipart form upload — the SDK builds the form for you. The cURL example below uses -Fto stand in for the SDK's FormData.

Browser — pass a File from an <input type="file">:

POST/api/storage/companies/{slug}/buckets/{slug}/media
const input = document.querySelector<HTMLInputElement>("input[type=file]")!
const file = input.files![0]
const uploaded = await sentroy.media.upload("product-assets", {
  body: file,
  folder: "products",
  tags: ["v1", "cover"],
})
console.log(uploaded.url) // Public CDN URL

Node.js — convert a file to a Blob via openAsBlob:

import { openAsBlob } from "node:fs"

const blob = await openAsBlob("./photo.jpg")
const uploaded = await sentroy.media.upload("product-assets", {
  body: blob,
  filename: "photo.jpg",
  isPublic: true,
})

Video processing — videos can opt into two server-side flags. compressVideo re-encodes the source at the same resolution (typically 30–60% smaller, no visible quality loss) and runs synchronously. transcodeVideo implies compressVideo and additionally generates a 144p / 480p / 720p / 1080p ladder. Ladder generation is asynchronous: the upload response returns immediately with processing.status === "queued", then ladder rungs stream into videoMeta.variants over the next few minutes. Ladder rungs are reachable at /f/<mediaId>/<height> (e.g. /f/abc/720).

POST/api/storage/companies/{slug}/buckets/{slug}/media (video, multi-quality)
const uploaded = await sentroy.media.upload("clips", {
  body: videoFile,
  compressVideo: true,
  transcodeVideo: true,
})

// Response is immediate; ladder fills in over time:
//   uploaded.processing.status        // "queued" | "processing" | "completed"
//   uploaded.processing.variantsTotal // 4
//   uploaded.videoMeta.variants       // [] at first, fills with each rung

// Poll until done:
let media = uploaded
while (
  media.processing &&
  media.processing.status !== "completed" &&
  media.processing.status !== "failed"
) {
  await new Promise((r) => setTimeout(r, 4000))
  media = await sentroy.media.get("clips", media.id)
}
console.log(media.videoMeta?.variants.map((v) => v.height))
// → [144, 480, 720, 1080]

Latency budget: compressVideo roughly doubles the upload handler latency (the request waits for ffmpeg). transcodeVideo only adds a probe + bookkeeping on the request path; the heavy ladder work runs in the background. Plan-tier quotas still meter the final compressed bytes, not the raw upload — a sub-second probe response keeps client UIs snappy even on very long videos.

Download#

Streams from the storage backend; works for both public and private buckets (auth-gated for private). Pass quality to fetch a pre-generated thumbnail size.

GET/api/storage/companies/{slug}/buckets/{slug}/media/{id}/download?quality=500
const blob = await sentroy.media.download("product-assets", mediaId)

const thumb = await sentroy.media.download("product-assets", mediaId, {
  quality: 500,
})

Delete#

DELETE/api/storage/companies/{slug}/buckets/{slug}/media/{id}
await sentroy.media.delete("product-assets", mediaId)

Removes the original S3 object, every generated thumbnail, and the media record in one call.

Thumbnails#

Image uploads automatically get several thumbnail sizes generated by the CDN. Use the helpers below to pick the right URL for your display target — never ship a 4000px JPG into a 56px avatar.

import {
  pickThumbnailUrl,
  pickPresetThumbnailUrl,
  THUMBNAIL_PRESETS,
} from "@sentroy-co/client-sdk"

// Manual target (px) — pass display size * 2 for retina
const avatarUrl = pickThumbnailUrl(media, 56 * 2)

// Semantic preset
const cardUrl = pickPresetThumbnailUrl(media, "card")       // ~500 px
const previewUrl = pickPresetThumbnailUrl(media, "preview") // ~960 px

How it picks

The helper picks the smallest thumbnail that still covers the target (so you never upscale), then falls back through:

  1. thumbnail.url if the backend exposed it directly
  2. CDN-prefix + thumbnail.fileName derived from media.url
  3. proxy media.downloadUrl?quality=N for private buckets
  4. media.url / media.downloadUrl if no thumbnails exist (non-image, or upload before processing finished)

Returns undefined only when the media has no public URL at all.

Presets

NameTypeDescription
avatar128 pxRound chips, 28-64 px display @2x
card500 pxGrid / list cards, 200-300 px display
preview960 pxModal / detail view
hero1600 pxFull-bleed hero, edge cases