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
| Visibility | Who can download | Signed URL lifetime |
|---|---|---|
private | Only the owner (the API key / account that owns it). | 15 minutes |
unlisted | Anyone with the link, but it is not listed publicly. | 24 hours |
public | Anyone 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:
- The API looks up the file. If it does not exist, is not
ready, or is deleted, it returns404. - For a
privatefile, the request must be the owner (authenticated). A request for someone else's private file returns404— the API does not reveal that the file exists. - For
public/unlisted, the request is allowed without auth. - 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=1From 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 URLOr 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.