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