Presigned file upload
Upload an image straight to storage with a signed URL, watch the progress, and see the server verify the bytes before anything can use them.
Before you try it
What it needs
R2_ACCOUNT_IDR2_ACCESS_KEY_IDR2_SECRET_ACCESS_KEYR2_BUCKETR2_PUBLIC_BASE_URLWithout them: Files are written under .uploads/ and served by a local route that mirrors R2's signed PUT and its signed GET, so the whole flow — private objects included — runs with no account.Mode right now
A safe failure to try
Upload an image
Local disk transportThe same component the settings page and the admin product form use. Uploads here are avatar-purpose, so any signed-in account may try it.
Sign in to upload
Storage is not a free file host: the presign endpoint refuses anonymous callers with a 401.
How this is wired
Three requests, and the bytes never touch this server.
1. Presign. The browser says what it wants to upload. The server checks who is asking and what for, records a pending row, and signs a URL good for one PUT of that one object for ten minutes.
2. PUT. The browser sends the file to that URL — R2 in production, a route handler that verifies the same kind of signature in development. Progress comes from XMLHttpRequest, because fetch cannot report upload progress.
3. Verify. The server reads the object’s real size and first bytes from storage. A PNG must start with the PNG signature. An HTML file named .png is deleted here, before anything can link to it.
Keys are random and carry the verified extension, so nothing about the uploader or the original filename reaches a public URL. SVG is refused entirely — it can carry scripts.
What this demonstrates
- Presign → PUT → verify: with R2 configured the bytes go straight to the bucket and never touch this server; without it the local transport accepts the same signed PUT, so development exercises the identical client
- A signed URL is authorization you can hand to a browser: one key, one type, expiring
- The local transport mirrors R2 with an HMAC-signed URL, so dev and prod share one client
- A file's real type comes from its first bytes, never its name or declared MIME type
- Authorization by purpose: product images need an admin; avatars need an account
- XMLHttpRequest, not fetch, because fetch has no upload progress events
Where the code lives
src/experiments/file-upload/demo.tsxRegistered in src/experiments/registry.ts and loaded on demand by loader.tsx.