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?
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
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?
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?
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?
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 });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
- 1empty
- 2empty
- 3empty
- 4empty
- 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?
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?
Go deeper
Pro tip
- 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.
- AWS, Download and upload objects with presigned URLs (the page the row names as Working with presigned URLs): https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html ·
aws-using-presigned.txt. "The credentials used by the presigned URL are those of the AWS Identity and Access Management (IAM) principal who generated the URL."; "You can use the presigned URL multiple times, up to the expiration date and time."; "If you use the AWS CLI or AWS SDKs, the expiration time can be set as high as 7 days."; "Amazon S3 checks the expiration date and time of a signed URL at the time of the HTTP request."; "if the connection drops and the client tries to restart the download after the expiration time passes, the download fails."; "presigned URLs are bearer tokens that grant access to those who possess them"; "verify that all request parameters—including the HTTP method, headers, and query string—match exactly between URL generation and usage." - AWS, Uploading objects with presigned URLs (named in the row): https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html ·
aws-presigned-upload.txt. "A presigned URL is limited by the permissions of the user who creates it." Supports step 2. - Cloudflare R2, Presigned URLs (named in the row): https://developers.cloudflare.com/r2/api/s3/presigned-urls/ ·
r2-presigned.txt. "authorizing anyone with the URL to perform a specific operation (like GetObject or PutObject) on a specific object until the URL expires."; "Presigned URLs are generated server-side with no communication with R2"; "uploads will fail with a 403/SignatureDoesNotMatch error if the client sends a different Content-Type for an upload request."; "Attempting to modify the resource, operation, or expiry will result in a 403/SignatureDoesNotMatch error."; "Treat presigned URLs as bearer tokens." - AWS, Uploading and copying objects using multipart upload (named in the row): https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html ·
aws-mpu-overview.txt. "It's a best practice to use multipart upload for objects that are 100 MB or larger". Supports step 1 (the parts themselves belong tofsd-file-upload-media). - AWS API Reference, PutObject: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html ·
aws-api-putobject.txt. "Amazon S3 never adds partial objects". - AWS API Reference, HeadObject: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html ·
aws-api-headobject.txt. "The HEAD operation retrieves metadata from an object without returning the object itself." Supports step 9. - AWS, Amazon S3 Event Notifications and Event message structure: https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html ·
aws-event-notifications.txt; https://docs.aws.amazon.com/AmazonS3/latest/userguide/notification-content-structure.html ·aws-event-structure.txt. "Amazon S3 event notifications are designed to be delivered at least once."; "The object size in bytes (as a number)". - MDN, Window: fetch() method: https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch ·
mdn-fetch-ok.txt. "A fetch() promise does not reject if the server responds with HTTP status codes that indicate errors (404, 504, etc.)."
Related lessons
Checked against the tools and docs as of 2026-10-12.