Provider gaps
A register of per-adapter runtime quirks that don't fit the capability matrix — signed-URL caps, ignored expiries, copy costs, and Content-Disposition support across every provider.
files.capabilities answers the clean yes/no questions — can this adapter range-read, sign a URL, copy server-side. This page is the register for everything that doesn't reduce to a flag: ceilings, silently-ignored options, and cost surprises that only show up against the real provider. It pairs with the capability matrix — check capabilities to branch in code, read this to understand the edges.
Signed URLs and expiry
capabilities.signedUrl.supported tells you whether url() can mint a signed or tokenized URL at all. The wrinkles below are about what happens to expiresIn once it can.
expiresIn is silently ignored
Some adapters return a working URL but don't honor the requested lifetime — the URL is either permanent or has a provider-fixed TTL.
- Vercel Blob — public blobs return the permanent CDN URL;
expiresInis ignored (there is no signing primitive). Private blobs have no public URL at all, sourl()throws — usedownload(). - Box —
url()returns a tokenized download URL, but Box controls its TTL server-side.expiresInis accepted for API symmetry and ignored. - PocketBase — file-token URLs have a server-controlled TTL (short, ~minutes).
expiresInis ignored. - Convex — serving URLs do not expire while the file exists.
expiresInis ignored.
expiresIn has a hard ceiling
- Dropbox — temporary links cap at 4 hours (14400s), enforced in code:
url()throws above that. UsepublicByDefault: truefor a permanent shared link instead. This ceiling is surfaced ascapabilities.signedUrl.maxExpiresIn. - Azure — user-delegation SAS (AAD credentials) cannot exceed 7 days from
startsOn; the adapter clamps the delegation key to that window. Account-key SAS has no such limit, so the cap is config-dependent and is not surfaced asmaxExpiresIn— it would be wrong in shared-key mode. - S3 and S3-compatible providers — SigV4 presigned URLs cap at 604800 seconds (7 days). This is an AWS infra limit rejected at request time, not enforced by the SDK, so it's not surfaced as
maxExpiresIn. S3-compatible providers (MinIO, Wasabi, and the others3()wrappers) may enforce a different limit or none.
url() requires explicit public-mode configuration
These adapters have no signing primitive; url() throws unless you opt into a permanent public link at construction:
- OneDrive / SharePoint — require
publicByDefault: true; Graph has no signed-URL primitive. The result is an anonymous (permanent) link. - Google Drive — requires
publicByDefault: true; returns a permanentuc?export=downloadlink. - Appwrite — requires
{ public: true }; returns a permanent view URL. Appwrite SDKs can't mint signed read URLs with API keys. - Bunny Storage — requires
publicBaseUrl(a Pull Zone / CDN host); the Storage API URL needs anAccessKeyheader and can't be handed out. - FTP / SFTP — require
publicBaseUrlpointing at an HTTP server fronting the same tree; the protocols serve no HTTP. - R2 (Workers binding) — requires either
publicBaseUrlor HTTP credentials (hybrid mode) for presigned URLs; a bare binding can't sign. - Netlify Blobs — has no public-URL primitive at all;
url()always throws. Usedownload().
Copy semantics
capabilities.serverSideCopy is true when copy() is a provider-side operation with no body re-transfer. When it's false, copy() still works but routes the bytes through your process — which matters for large objects and serverless memory limits.
- UploadThing —
copy()buffers the whole object in memory (the upload API needs aBlob). A multi-GB copy will exhaust serverless memory; do it at the application layer instead. - SFTP —
copy()buffers the whole object over a single connection. FTP streams instead but still round-trips the bytes through the client. - Bun S3 — Bun's S3 client has no
CopyObjecthelper, socopy()streams source→dest through the process rather than issuing a server-side copy. - R2 (Workers binding) — bindings have no server-side copy;
copy()streamsget→put. Source and destination are not atomic. - Bunny Storage / Netlify Blobs — no native copy;
copy()reads the source and re-writes the body at the destination. Not atomic. - PocketBase / Appwrite —
copy()downloads the source and creates a new record/file. Not atomic. - Cloudinary — Cloudinary has no native copy;
copy()re-uploads by URL, which produces a new asset with a newasset_idandetag— not a byte-identical reference. The source must be fetchable by Cloudinary. - Convex —
copy()is unsupported and throws: storage ids are immutable and can't be assigned to a caller-chosen key. Download the source and upload it back, tracking the new id.
responseContentDisposition
The url() option that forces a download (and prevents stored-XSS on user-uploaded HTML/SVG) requires a signature to bind into. Adapters that can sign support it — S3 and the S3-compatible wrappers, Azure, GCS, Firebase Storage, Supabase, and R2 in HTTP / hybrid mode. The rest throw rather than silently dropping the security ask: Vercel Blob, Dropbox, Box, OneDrive / SharePoint, Google Drive, UploadThing, Cloudinary, Convex, Bunny Storage, PocketBase, FTP, and SFTP. For buckets with untrusted content, pick a provider that can sign.
Uncommitted uploads
- Azure — uncommitted blocks (from an interrupted multipart / staged-block upload) are garbage-collected by Azure after ~7 days. A resume past that window starts fresh rather than continuing.
Capabilities
Query what an adapter can do up front with files.capabilities — branch on range reads, signed URLs, server-side copy, and more instead of waiting for a throw at call time.
FAQ
Common questions about supported providers, bundling and peer dependencies, switching backends, browser uploads, error handling, and the CLI.