Tanstack Start

Access control

Visibility levels, the /f/:id URL, and how downloads are authorized.

Every file has a visibility that decides who can download it. Access is always decided by the file's database row — never by guessing the bucket path, which is random and unguessable.

Visibility levels

VisibilityWho can downloadSigned URL lifetime
privateOnly the owner (the API key / account that owns it).15 minutes
unlistedAnyone with the link, but it is not listed publicly.24 hours
publicAnyone with the link.24 hours

The default is private. Set it at upload time, or change it later.

// At upload
await s3.upload(blob, { name: "avatar.png", visibility: "public" });

// Later
await s3.update(fileId, { visibility: "private" });

From the browser, set visibility on the uploader (subject to whatever your route handler's beforeUpload allows):

useUploadFiles({ visibility: "public" });

beforeUpload has the final say

The route handler can override or pin visibility server-side. A common pattern is to force visibility: "private" in beforeUpload so a client can never make a file public on its own.

The /f/:id URL

Every file's url is a stable link of the form https://api.s3.delivery/f/{id}. Hitting it returns a 302 redirect to a freshly-signed bucket URL. Because it re-signs on every request, you can store file.url anywhere — a database, an <img src>, an email — and it keeps working long after any single signed URL has expired.

// The stable URL works in markup directly.
<img src={file.url} alt={file.name} />;
<a href={file.url}>Download {file.name}</a>;

How the redirect is authorized:

  1. The API looks up the file. If it does not exist, is not ready, or is deleted, it returns 404.
  2. For a private file, the request must be the owner (authenticated). A request for someone else's private file returns 404 — the API does not reveal that the file exists.
  3. For public / unlisted, the request is allowed without auth.
  4. The API signs a short-lived GET URL and redirects to it.

Forcing a download

Append ?download=1 to get an attachment response (Content-Disposition: attachment) instead of an inline one — the browser downloads the file rather than rendering it.

https://api.s3.delivery/f/file_abc123?download=1

From the SDK, use the download option:

const { url } = await s3.get(fileId, { download: true });

Private files in your own app

To serve a private file to its owner, mint a fresh signed URL on your server (the API verifies ownership via the API key) and hand it to the browser:

// Server route, after you've authenticated the user as the owner.
const { url } = await s3.get(fileId);
return Response.json({ url }); // short-lived (15 min) signed URL

Or redirect the browser through the stable /f/:id URL while authenticated as the owner — the same authorization applies.

Signed URL lifetime

Signed URLs are deliberately short-lived so a leaked link stops working quickly: 15 minutes for private files, 24 hours for public/unlisted. Always store the stable /f/:id URL for anything long-lived, and treat the signed URL from get / download as ephemeral.

On this page