Skip to content
Files SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Capabilities

Query what an adapter can do up front with files.capabilities - including range reads, signed URLs, server-side copy, and native conditional operations.

The unified surface is the common subset every adapter implements, but adapters differ at the edges: some honor byte-range reads, some can mint a signed URL, some copy server-side. The wrapper already gates on these per-adapter — pass a range to an adapter that has no range primitive and it throws before any provider call. files.capabilities turns that implicit knowledge into a queryable surface, so you can branch up front instead of discovering a limit by catching an error.

const files = new Files({ adapter: r2({ bucket: "uploads" }) });

if (files.capabilities.rangeRead) {
  // Safe to stream a byte range — the adapter has a range primitive.
  const clip = await files.download(key, { range: { start: 0, end: 1023 } });
}

if (files.capabilities.signedUrl.supported) {
  return files.url(key, { expiresIn: 600 });
}
// No signing primitive — stream the bytes through the SDK instead.
return files.download(key);

The shape

interface AdapterCapabilities {
  rangeRead: boolean;
  uploadProgress: boolean;
  delimiter: "any" | "slash" | false;
  metadata: boolean;
  publicUrl: boolean;
  cacheControl: boolean;
  resumable: boolean;
  serverSideCopy: boolean;
  events: { format: EventFormat } | false;
  signedUrl: {
    supported: boolean;
    expiry: "exact" | "provider" | "none";
    disposition: boolean;
    maxExpiresIn?: number;
  };
  signedUpload: {
    supported: boolean;
    maxSize: boolean;
    contentType: boolean;
    maxExpiresIn?: number;
  };
  conditional: {
    create: boolean;
    replace: boolean;
    exactRead: boolean;
    delete: boolean;
    copy: {
      sourceEtag: boolean;
      destinationCreate: boolean;
      destinationReplace: boolean;
      atomicSourceDestination: boolean;
    };
    multipart: { create: false; replace: false };
  };
}
Field true means Maps to
rangeRead download({ range }) returns only the requested bytes download
uploadProgress upload({ onProgress }) reports byte-level progress natively upload
delimiter list({ delimiter }) returns S3-style common prefixes, for any delimiter ("any") or only "/" ("slash") list
metadata upload({ metadata }) persists arbitrary user metadata upload
cacheControl upload({ cacheControl }) stores a Cache-Control header upload
resumable the adapter exposes a pause-able, resumable upload primitive upload({ control })
serverSideCopy copy() runs server-side, with no body re-transfer through your process copy
publicUrl a plain url(key) returns a permanent link (a publicBaseUrl CDN, a public bucket or blob, a public share link) url
events the provider sends bucket notifications in this format ("s3", "r2", "gcs", …), for files-sdk/events to parse files.events
signedUrl url() can mint a signed or tokenized URL — see below url
signedUpload signedUploadUrl() hands out a direct upload target — see below signedUploadUrl
conditional the requested create, replace, exact-read, delete, or copy predicate has a provider-native primitive upload, download, delete, copy

Every field mirrors an operation the unified API actually has. There are deliberately no flags for raw-only territory such as object versioning, checksums, or POST policies — advertising a flag for a non-operation would turn the matrix into a back-door spec, where a wrong flag is worse than no flag.

conditional

Conditional capabilities are granular because providers often support only part of the surface. create, replace, exactRead, and delete map to their native single-object primitives. Conditional copy separately declares source ETag checks, destination create/replace checks, and whether the selected source plus destination predicates are evaluated atomically in one request. Invocation rechecks the declaration and fails before provider I/O when any required part is absent. These flags start from native adapter support, and every bundled plugin that vetoes a conditional mode (because its own policy cannot preserve that atomic boundary) also clears the matching flag - see below. A third-party plugin can still veto a mode without narrowing it.

There is no probe-then-mutate fallback. See Conditional operations for the API, ETag rules, provider matrix, and retry/observer semantics.

signedUrl

signedUrl: {
  supported: boolean;
  expiry: "exact" | "provider" | "none";
  disposition: boolean;
  maxExpiresIn?: number;
}

supported is true when url() mints a signed or tokenized download URL that grants access without the caller’s own credentials and is more than a permanent public link — an S3 SigV4 URL, an Azure SAS, a GCS signed URL, a Box or PocketBase access-token URL. It’s false when the adapter has no signing primitive: it returns only a permanent public URL (Vercel Blob in public mode, UploadThing public-read, Appwrite, Convex, OneDrive / Google Drive / SharePoint in public-link mode) or throws because it can’t mint one at all (FTP/SFTP/WebDAV without publicBaseUrl, Netlify Blobs, OneDrive / Google Drive outside their public-link mode). The local filesystem returns an unsigned file:// or static-server URL. When false, prefer download().

maxExpiresIn is set only when the adapter enforces a hard ceiling on expiresIn in code, throwing above it. Dropbox temporary links cap at 4 hours (14400s). The S3 family — s3() and its S3-compatible wrappers, R2 over HTTP or in hybrid mode, the fetch engine, and Bun’s S3 — caps presigned URLs at SigV4’s 604800 seconds (7 days). Google Cloud Storage and Firebase Storage declare the same 7-day V4 signing cap, and so does UploadThing with acl: "private". Azure declares its 7-day cap only in user-delegation mode, since account-key SAS has no such limit. It stays unset where the limit isn’t checked in code; those cases live in Provider gaps, not here. expiry says what expiresIn does on this instance: "exact" when the URL expires after exactly that many seconds, "provider" when the URL is tokenized but the provider sets its lifetime (Box, PocketBase, Dropbox’s 4-hour links), and "none" when url() only returns permanent links (it’s always "none" when supported is false, and then an explicit expiresIn throws Unsupported). disposition is true when a responseContentDisposition is bound into the signed URL; when it’s false, passing one throws, so the gateway proxies downloads it must force attachment on.

publicUrl

true when a plain url(key), with no expiresIn and no responseContentDisposition, returns a permanent link on this instance: a configured publicBaseUrl, Vercel Blob in public mode, UploadThing’s public-read, Supabase or Appwrite with public: true, a public share link (publicByDefault on Box, Dropbox, OneDrive, Google Drive), Convex serving URLs, or the filesystem’s urlBaseUrl URL (a file:// link doesn’t count). It’s independent of signedUrl: the S3 family with a publicBaseUrl has both, returning the CDN link for a plain url(key) and a signed one when you pass expiresIn.

events

The bucket-notification format this instance’s provider sends, or false when it sends none that files-sdk/events can read. events() reads it to parse deliveries. It’s declared only where the provider is verified to send that format for the configuration: s3() against AWS reads "s3", but s3() with a non-AWS endpoint reads false (an explicit amazonaws.com endpoint, or an AWS_ENDPOINT_URL_S3 / AWS_ENDPOINT_URL pointing at AWS, still counts as AWS), since an S3-compatible service may not send S3’s shape. MinIO, RustFS, Wasabi, and Storj, which do, declare it themselves. See Events for every format.

signedUpload

signedUpload: {
  supported: boolean;
  maxSize: boolean;
  contentType: boolean;
  maxExpiresIn?: number;
}

supported is true when signedUploadUrl() returns a presigned target a client can upload to directly. maxSize is true when a maxSize is enforced server-side (an S3 POST policy, a Vercel Blob size limit); when it’s false, passing maxSize throws rather than minting an unbounded URL. contentType is true when a contentType is bound into the signature; when it’s false, passing one throws. The gateway reads this to decide between a direct upload and its proxy path: it presigns only when the adapter can bind the configured maxUploadSize and the file’s content type, and proxies the upload through the instance otherwise.

How it’s derived

Each adapter declares what it can do as a single capabilities object, per instance — so a constructor option that changes what it can do (a publicBaseUrl, an access mode, the fetch client) is reflected. files.capabilities reads that declaration live on every call, filling in the conservative value (false, or unsupported) for anything the adapter leaves out, so an adapter that says nothing never reads as a wrong “yes”. resumable and conditional are derived from the methods that implement them (resumableUpload, and the adapter’s native conditional operation group with its explicit copy guarantees). The wrapper gates on this same snapshot, so it can’t drift from runtime behavior.

Capabilities also reflect the instance’s installed plugins. After reading the adapter, the getter folds each plugin’s capabilities hook over the snapshot in plugins order, and a plugin can only narrow it. encryption() and compression() turn off signedUrl, signedUpload, rangeRead, and resumable, dedup() turns off signedUrl, signedUpload, and every conditional flag, validation() and contentType() turn off signedUpload, tiering() and failover() turn off every conditional flag, versioning() turns off every conditional flag except exactRead, and softDelete() turns off conditional.delete, because the plugin can’t honor those paths end to end. So the same adapter can report different capabilities on two instances with different plugins.

The wrapper gates options on the narrowed snapshot before any plugin runs. Pass a range to an instance whose adapter has no range primitive, or whose encryption() plugin turns ranges off, and the call throws before a plugin can do any I/O, with an error naming whichever one refused.

Declaring capabilities on a custom adapter

A custom adapter declares its capabilities as one object. Every field is optional and defaults to unsupported:

const myAdapter: Adapter = {
  name: "my-store",
  raw: client,
  capabilities: {
    rangeRead: true,
    delimiter: "slash",
    metadata: true,
    serverSideCopy: true,
    publicUrl: Boolean(publicBaseUrl),
    signedUrl: { supported: true, disposition: true, maxExpiresIn: 86_400 }, // expiry defaults to "exact"
    signedUpload: { supported: true, maxSize: true, contentType: true },
    events: { format: "s3" }, // only if the provider is verified to send S3 notifications
  },
  // upload, download, head, exists, delete, copy, list, url, signedUploadUrl…
};

Declare what the instance can actually do with its configuration. A rangeRead, metadata, cacheControl, or delimiter you declare is let through to your adapter; one you leave out is refused before your adapter is called. serverSideCopy, signedUpload, and events are descriptive: they don’t gate the call, but callers, the gateway and files-sdk/events plan around them, so a wrong true sends them down a path that fails. signedUrl.supported gates url(key, { expiresIn }): leave it false and that call is refused with Unsupported before your adapter runs.

Last updated on

Was this page helpful?