# How it works (/docs/architecture)



s3.delivery is built so that **file bytes never pass through any API** — not
yours, not ours. The system has three parts with clearly split jobs.

| Part                       | Role                                                                                                                                     |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Worker API**             | Security guard and signer. Checks auth and quota, enforces constraints, mints presigned URLs, tracks metadata. Never touches file bytes. |
| **Database (D1)**          | Source of truth: ownership, status, visibility, and size. Access is always decided here — never from the bucket path.                    |
| **Bucket (S3-compatible)** | Stores the actual bytes. Browsers upload and download directly.                                                                          |

## Upload flow [#upload-flow]

```
Browser ──ask──▶ Worker API ──▶ D1 (pending row + presigned URL)
Browser ════════ PUT bytes ════════▶ Bucket        (the cheap, direct path)
Browser ──done─▶ Worker API ──▶ verify object exists + mark ready
```

1. The browser (via your [route handler](/docs/route-handler)) asks the API for
   an upload URL — `createUpload` with the filename, content type, and size.
2. The API checks the API key, enforces your route constraints and the plan
   limits, writes a `pending` row to the database, and returns a **presigned PUT
   URL** (one URL for small files, a set of part URLs for large ones).
3. The browser `PUT`s the bytes **straight to the bucket**. Nothing flows through
   the API.
4. The browser calls `complete`. The API confirms the object really exists in the
   bucket, records its real size and checksum, and flips the row to `ready`.

Because completion is a normal request to your own server, the
[`onUploadComplete`](/docs/route-handler#onuploadcomplete) callback runs inline —
no webhook endpoint, no public callback URL, no dev tunnel.

### Multipart for large files [#multipart-for-large-files]

Files at or above **100 MB** switch to multipart automatically. The API initiates
the multipart upload and returns a presigned URL per **16 MB** part; the browser
uploads parts **concurrently** (6 at a time by default) and the API stitches them
together on `complete`. You do not configure any of this — see
[Large files & multipart](/docs/large-files).

## Download flow [#download-flow]

1. The browser asks for a file — `get(fileId)`, or simply navigates to the stable
   `/f/:id` URL.
2. The API checks the database: the requester is the owner, or the file is
   `public` / `unlisted`.
3. The API signs a short-lived GET URL **locally** (no extra round-trip to
   storage) and returns it, or redirects `/f/:id` to it with a `302`.
4. The browser downloads directly from the bucket.

Signed download URLs are short-lived: **15 minutes** for private files and **24
hours** for public/unlisted ones. The `/f/:id` URL itself is stable and safe to
store — it re-signs on every request.

## Why it is cheap and fast [#why-it-is-cheap-and-fast]

* **No proxy bandwidth.** Bytes never pass through an API, so there is no egress
  to pay for on a server and no CPU/memory bottleneck to rate-limit you.
* **Storage is the only real cost.** Object storage is billed per GB-month —
  pennies per GB.
* **The API stays tiny.** Each request is a small signing plus a database read or
  write, so latency stays low and throughput scales with your bucket, not a proxy.
* **Parallelism by default.** Batches upload several files at once and large files
  upload parts concurrently, so a slow file never stalls the rest.

You can measure it yourself on the **Speed Test** page in the dashboard, or with
the bundled benchmark that compares raw bucket throughput against the full wrapped
SDK path.

## Security model [#security-model]

* **Bucket credentials live only on the server.** The browser never sees them —
  only short-lived presigned URLs.
* **The database is always consulted before signing a download.** Private files
  require the owner; a request for someone else's private file returns `404` (we
  do not leak that it exists).
* **Object keys are not user-controlled.** Keys are
  `users/{userId}/files/{fileId}/{safeName}`, where `fileId` is a random,
  unguessable id and the filename is sanitized. Access is decided by the database
  row, never by the path.
* **Keys are stored hashed.** API keys are kept as a SHA-256 hash; the plaintext
  is shown exactly once at creation. See [API keys](/docs/api-keys).
* **Abandoned uploads are cleaned up.** A scheduled job sweeps `pending` rows
  older than an hour, deleting any orphaned object and marking the row deleted, so
  a reserved-but-never-finished upload never lingers or counts against quota.

## File lifecycle [#file-lifecycle]

A file row moves through three states:

| Status    | Meaning                                                                                      |
| --------- | -------------------------------------------------------------------------------------------- |
| `pending` | Reserved by `createUpload`; bytes may still be in flight. Not yet downloadable.              |
| `ready`   | `complete` confirmed the object exists. Downloadable, counts toward usage.                   |
| `deleted` | Soft-deleted by `delete` or swept by the cleanup job. The object is removed from the bucket. |

Only `ready` files appear in [`list`](/docs/server-client#list) and count toward
your storage usage.
