signedUploadUrl

A presigned PUT-or-POST contract so a browser can upload straight to the bucket - bandwidth and CPU stay off your server.

files.signedUploadUrl(key, options)

Returns a discriminated PUT-or-POST contract so a client (typically a browser) can upload directly to the bucket without proxying bytes through your server. The flow is: your server calls signedUploadUrl(), returns the result to the browser, the browser uploads straight to the provider directly. Bandwidth and CPU stay off your server.

Without maxSize, the adapter returns a presigned PUT URL - simpler, but with no server-side size cap. With maxSize, the adapter switches to a presigned POST form whose policy enforces the size at the bucket via content-length-range. In practice you should always pass maxSize - without it, anyone with the URL can DoS your storage costs until expiresIn elapses.

Vercel Blob, Bunny Storage, Appwrite, and PocketBase throw here - Vercel's upload model goes through handleUpload() from @vercel/blob/client instead of presigned URLs, Bunny Storage writes require the Storage API AccessKey header, and Appwrite/PocketBase have no presigned upload primitive at all (mint a short-lived auth token for the client instead). The R2 Workers binding throws unless you've configured hybrid mode (binding + HTTP credentials). Azure, Supabase, and R2 have no content-length-range equivalent and throw if you pass maxSize — Azure and Supabase have no server-enforced size limit, and Cloudflare R2 does not implement the S3 POST Object API (a presigned POST would fail with 501 Not Implemented at upload time), so omit maxSize for a presigned PUT. UploadThing returns a PUT URL and treats maxSize as advisory rather than enforced — it caps uploads via the file-router config tied to the adapter's slug instead of via the URL signature. Enforce upload caps at your application gateway (or at the provider's dashboard-level bucket/route setting).

// On your server: hand back an upload contract that lets the browser
// PUT/POST the file directly to the bucket. Bytes never touch your server.
const upload = await files.signedUploadUrl("avatars/abc.png", {
  expiresIn: 60,
  contentType: "image/png",
  maxSize: 5_000_000,
});
// → { method: "PUT", url, headers? }
//   | { method: "POST", url, fields }

// In the browser: PUT path (no maxSize) is a plain fetch.
await fetch(upload.url, {
  method: "PUT",
  body: file,
  headers: upload.headers,
});

// POST path (with maxSize) is multipart with the signed policy fields.
const form = new FormData();
for (const [k, v] of Object.entries(upload.fields)) form.append(k, v);
form.append("file", file);
await fetch(upload.url, { method: "POST", body: form });

Options

Prop

Type

On this page