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.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.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 },
});