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. 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, providers that support upload policies switch to a presigned POST form whose policy enforces the size at the bucket via content-length-range. In practice you should pass maxSize when the adapter supports it - without it, anyone with the URL can DoS your storage costs until expiresIn elapses.
Bunny Storage, Appwrite, PocketBase, Netlify Blobs, Box, Dropbox, FTP, SFTP, WebDAV, fs, and Convex throw here - Bunny Storage writes require the Storage API AccessKey header, Appwrite/PocketBase/Netlify Blobs have no presigned upload primitive at all, Box and Dropbox uploads need a request shape the PUT-or-POST contract can’t express, FTP/SFTP/WebDAV have no presigned-upload concept, fs has no signer/verifier-backed upload server, and Convex upload URLs cannot bind the caller’s SDK key or constraints. The R2 Workers binding throws unless you’ve configured hybrid mode (binding + HTTP credentials). Azure, Supabase, R2, Bun S3, the fetch S3 engine (s3Fetch() and client: "fetch"), Google Drive, OneDrive, SharePoint, Cloudinary, and UploadThing have no content-length-range equivalent and throw if you pass unsupported size limits; omit those options for a presigned PUT/session URL and enforce upload caps at your application gateway instead. Azure, Bun S3, and Cloudinary also throw if you pass contentType, because none of them binds Content-Type into the signature. Cloudinary and UploadThing also reject a positive minSize. Vercel Blob mints a presigned PUT via Vercel Signed URLs that enforces contentType and maxSize at the CDN, but has no minimum-size constraint, so a positive minSize throws.
// 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: branch on `method` to narrow the contract.
if (upload.method === "PUT") {
// PUT path (no maxSize) is a plain fetch.
await fetch(upload.url, {
method: "PUT",
body: file,
headers: upload.headers,
});
} else {
// 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
expiresInnumber
How long the signed URL stays valid, in seconds. After it elapses, the URL stops working and the client must request a new one.
numbercontentType?string
MIME type bound into the signature when the provider supports doing so. Adapters that cannot enforce it at the signed URL layer throw rather than returning an advisory header.
stringmaxSize?number
Maximum upload size in bytes, enforced server-side. **Strongly recommended when supported.** When omitted, the adapter falls back to a presigned PUT URL with no server-side size limit — anyone with the URL can upload an arbitrarily large file until `expiresIn` elapses. When set, supporting adapters use a presigned POST form (S3 and the S3-compatible adapters on the `aws-sdk` client) that enforces the size via a `content-length-range` policy. Adapters whose direct-upload primitive cannot enforce this (R2, the `fetch` S3 client, …) fail closed.
numberminSize?number
Minimum upload size in bytes for the presigned POST policy. Defaults to `1` — empty uploads are usually a sign of a broken client, and the most common application assumption ("file present means real content") fails silently when 0-byte objects can land. Pass `0` if you genuinely want to allow empty uploads. Only used by adapters that can enforce `maxSize`; adapters whose direct-upload primitive cannot enforce this fail closed.
numbersignal?AbortSignal
Abort the operation when this signal is aborted. When both constructor and per-call signals are provided, either one can abort the call.
AbortSignaltimeout?number
Timeout in milliseconds, applied to each attempt (not the call as a whole). A timeout aborts the operation and is not retried. `0` or a negative value disables timeout handling.
numberretries?RetryOptions
Retry provider failures. A number is treated as `{ max: number }`.
RetryOptions