Skip to content

File uploads through presigned URLs

A presigned URL is a short-lived bearer credential for one object: authorise and scope it before you sign, then believe storage, not the client.

Predict first

Before you read: predict

ExamHall's API signs a presigned PUT URL for each 10-second chunk of a candidate's proctoring video, valid for 15 minutes, a few chunks ahead of the recorder. The URL for chunk 7 ends up in the client's error report, which every support agent can open. Two minutes later, before the client has sent chunk 7, an agent checks whether the link works: from a terminal in the office, signed in to nothing, it PUTs a 4-byte test file to the URL.

What does storage do?

Pick one

The 30-second version

The API checks who is asking, then signs a URL for one key under that caller's own prefix, one type and length, for a few minutes. Storage checks only the signature and its clock, so the URL is a bearer credential: anyone holding it can use it until it expires. Record the file from storage, never the client's word.

A presigned URL is a short-lived bearer credential for one object: authorise and scope it before you sign, then believe storage, not the client.

How it works

Step 1 of 10

Bytes skip the API. The API authorises and signs; the browser sends the file straight to storage. Large files go in parts (fsd-file-upload-media step 1).
In the kitchen
  • no tent passes through the park office → the bytes skip the API (step 1)

Analogy: A national park's backcountry camping permits

In the park In the upload
no tent passes through the park office the bytes skip the API (step 1)
the office checks the hiker's own trip booking before writing a permit authorise first (step 2)
the permit names one pitch on that hiker's route, the group size, "tents only" and the night the server names the key; type and length are signed (steps 3, 8)
the ranger checks the embossed seal and the details at the pitch, without radioing the office storage checks the URL alone (step 4)
a permit dropped at the trailhead lets whoever finds it camp there that night a bearer credential (step 5)
a group that arrives on its night stays till morning; one that comes back the next evening is turned away expiry read when a request starts (step 7)
the office counts a night as camped from the ranger's log, not from the hikers' postcard record from storage (step 9)

In real terms: the API decides and signs; storage enforces exactly what was signed, and nothing else.

Breaks down when: a ranger can recognise a face, while storage sees only the URL.

Contrasting case: streaming the file through the API keeps your own session check on every byte, at the cost of carrying every byte (fsd-file-upload-media step 2).

Examples, run for real

4.1 ExamHall EXH-3680 → MR !3980: a chunk with no size limit, then type and length signed

ExamHall's sign endpoint signed the key alone. A candidate's recorder stopped rotating chunks, and one 96 MiB chunk went up where chunks are at most 8 MiB (EXH-3680). MR !3980 has the API refuse to sign for more than 8 MiB and sign the type and the exact length.

4.1 EXH-3680, key signed alone: a 100,663,296-byte chunk -> PUT 200 · stored 100,663,296 bytes (chunks are at most 8,388,608)
4.1 MR !3980, type and length signed, 8 MiB limit: sign request for 100,663,296 bytes -> 413, no URL · 100,663,296 bytes sent to a URL signed for 3,145,728 -> PUT 403 SignatureDoesNotMatch, nothing stored
4.1 MR !3980, a normal 3,145,728-byte chunk -> PUT 200 · stored 3,145,728 bytes · file bytes the API read 0 (JSON only, 131 bytes)

The API refuses what it can see; storage refuses a body that differs from what was signed (step 8). No file byte crosses the API (step 1).

4.2 Proof domain, media streaming: a creator's video, recorded from storage

A video platform signs an upload URL for a creator's 50 MiB video and records it from storage's ObjectCreated event.

4.2 sign: 1 request to the API, key uploads/cr-9/v-1.mp4 · PUT straight to storage 200, requests to the API while storage checked the URL 0
4.2 ObjectCreated event: key uploads/cr-9/v-1.mp4, size 52,428,800 · row recorded from the event: 52,428,800 bytes · file bytes the API read 0

Storage checked the URL with no call to the API (step 4), and the row carries the size storage reported (step 9).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

ExamHall's design so far: the proctoring client calls POST /uploads/sign with { key, contentType, size } and gets back a presigned PUT URL for that key; every candidate is signed in. The interviewer's next follow-up: "Candidate c-2 sends a key under attempt a-1's prefix, which belongs to candidate c-1. What keeps c-2's bytes out of it?" You may change only the sign endpoint.

Which change answers the follow-up?

Pick one

Question 2 of 7

A video platform's API signs a PUT URL for uploads/cr-9/v-41.mp4, with Content-Type: video/mp4 and Content-Length: 5242880 among its signed headers, valid for 10 minutes. One minute later the creator's app PUTs a 5,242,880-byte file to it with Content-Type: video/quicktime.

What does storage do?

Pick one

Question 3 of 7

ExamHall signs each chunk URL for the session's own attempt, valid for 15 minutes, and the proctoring client runs:

In one session, c-1's chunk goes up at once, c-2's laptop sleeps for 20 minutes between sign and the PUT, and c-3 closes the tab right after sign. The reviewers' list must hold exactly the chunks storage holds.

Which way of recording meets that?

Code for this question
const { key, url } = await api.post('/uploads/sign', { contentType: 'video/webm', size: chunk.size });await fetch(url, { method: 'PUT', body: chunk, headers: { 'Content-Type': 'video/webm' } });await api.post('/uploads/recorded', { key, size: chunk.size });
Pick one

Question 4 of 7

A video platform's API handles a creator's video upload into the upload slot the creator opened, without the bytes passing through it.

In what order does the platform run these steps?

Your order

  1. 1empty
  2. 2empty
  3. 3empty
  4. 4empty
  5. 5empty

Tap the lines in the order you expect.

Question 5 of 7

The interviewer says: "Candidates' browsers upload 10-second proctoring chunks straight to storage. Design the signing endpoint." In 15 minutes, sketch your board: who names the object, how long a URL lives, what is signed into it, the largest chunk you sign for, and when a chunk is recorded.

Then write your choices in the shape of design.template.mjs as my-design.mjs, and run the check.

Question 6 of 7

ExamHall's proctoring client uploads a candidate's 72 MB end-of-exam recording with one presigned PUT, signed with a 15-minute expiry. Over the candidate's slow uplink the upload takes 10 minutes, and it starts 14 minutes after the URL was signed. At minute 21 the connection drops. At 21:30 the client sends the whole recording again to the same URL.

What happens?

Pick one

Question 7 of 7

A video platform keeps each creator's monthly earnings statement, a PDF, in storage under statements/<creator>/. A creator clicks Download, and an hour later forwards the link the click used to an accountant, who opens it at once.

With which design does the forwarded link fail?

Pick one

Go deeper

Pro tip
  1. When asked how you constrain a presigned URL, list what you sign before anyone asks about attacks: the key under the caller's prefix, the type, the length and the minutes it lives. When: the interviewer asks what can go wrong with presigned URLs. Status: SELF; not run.
Source

Valid as of 2026-10-12. Every rule of the store model is quoted from these pages in lib/store.mjs.

Checked against the tools and docs as of 2026-10-12.