Skip to content
Interactive demoAPIsAdvanced

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.

Built withCloudflare R2aws4fetchXMLHttpRequest

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

FallbackThis deployment runs the plain path, so what you try here costs nothing and reaches no provider.

A safe failure to try

Choose a file that is not an image: it is refused before a signed URL is even requested.

Upload an image

Local disk transport

The 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.tsx

Registered in src/experiments/registry.ts and loaded on demand by loader.tsx.