Skip to content
Files SDK
Esc
navigateopen⌘Jpreview
On this page

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, 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.

Vercel Blob, Bunny Storage, Appwrite, PocketBase, fs, and Convex 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, Appwrite/PocketBase have no presigned upload primitive at all, 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, 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 also throws if you pass contentType, because SAS does not bind Content-Type into the signature.

// 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

PropType
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.

Typenumber
contentType?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.

Typestring
maxSize?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/R2) that enforces the size via a `content-length-range` policy. Adapters whose direct-upload primitive cannot enforce this fail closed.

Typenumber
minSize?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.

Typenumber
signal?AbortSignal

Abort the operation when this signal is aborted. When both constructor and per-call signals are provided, either one can abort the call.

TypeAbortSignal
timeout?number

Overall timeout in milliseconds, applied to each attempt. A timeout aborts the operation and is not retried. `0` or a negative value disables timeout handling.

Typenumber
retries?RetryOptions

Retry provider failures. A number is treated as `{ max: number }`.

TypeRetryOptions

Was this page helpful?