Storage (presigned uploads)

The server mints a presigned PUT URL; the browser uploads directly to S3/R2 — bytes never traverse bext. Same flow for reads: the server mints a presigned GET, the page redirects or links to it. Backend choice (S3, R2, local) is a config switch.

Tip
Presigned URLs expire (300s here) and are scoped to the exact key. Never cache them client-side or log them — they grant temporary access without further authentication.
Backend not configured on this site. The form below will show the error path; the API surface and config shape are accurate, but no actual upload happens until [storage] is set in bext.config.toml. Add the snippet under Configuration, restart, and the demo becomes live.

Try it

backend not configured

bext.config.tomlTOML
# bext.config.toml — wire up an S3-compatible backend.
[storage]
provider    = "s3"          # or "r2", "local"
bucket      = "my-bucket"
region      = "us-east-1"
endpoint    = "https://s3.amazonaws.com"
access_key  = "AKIA..."     # use an env var in production
secret_key  = "..."
public_base = "https://cdn.example.com"  # optional
src/app/examples/storage/page.tsxTSX
// src/app/examples/storage/page.tsx — server: mint presigned URLs.
import { presignPut, presignGet, publicUrl } from "@bext-stack/framework/storage";

export async function action({ request }) {
  const key = "demo/" + Date.now() + ".txt";
  return Response.json({
    putUrl: presignPut(key, { ttlSecs: 300 }),
    getUrl: presignGet(key, 300),
    publicUrl: publicUrl(key),  // null when backend has no public URL
    key,
  });
}

// Client: PUT directly to S3/R2 — bytes never traverse bext.
const r = await fetch("/examples/storage", { method: "POST", body: form });
const { putUrl } = await r.json();
await fetch(putUrl, {
  method: "PUT",
  body: file,
  headers: { "content-type": file.type },
});