# Access control (/docs/access-control)



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-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.

```ts
// 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`](/docs/route-handler#beforeupload) allows):

```tsx
useUploadFiles({ visibility: "public" });
```

<Callout title="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.
</Callout>

## The `/f/:id` URL [#the-fid-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.

```tsx
// 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 [#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:

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

## Private files in your own app [#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:

```ts
// 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-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.
