How it works
The browser talks to the bucket. The API only signs and authorizes.
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
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- The browser (via your route handler) asks the API for
an upload URL —
createUploadwith the filename, content type, and size. - The API checks the API key, enforces your route constraints and the plan
limits, writes a
pendingrow to the database, and returns a presigned PUT URL (one URL for small files, a set of part URLs for large ones). - The browser
PUTs the bytes straight to the bucket. Nothing flows through the API. - The browser calls
complete. The API confirms the object really exists in the bucket, records its real size and checksum, and flips the row toready.
Because completion is a normal request to your own server, the
onUploadComplete callback runs inline —
no webhook endpoint, no public callback URL, no dev tunnel.
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.
Download flow
- The browser asks for a file —
get(fileId), or simply navigates to the stable/f/:idURL. - The API checks the database: the requester is the owner, or the file is
public/unlisted. - The API signs a short-lived GET URL locally (no extra round-trip to
storage) and returns it, or redirects
/f/:idto it with a302. - 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
- 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
- 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}, wherefileIdis 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.
- Abandoned uploads are cleaned up. A scheduled job sweeps
pendingrows 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
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 and count toward
your storage usage.