---
title: API
description: The plugin contract types and helpers - FilesPlugin, FilesOperation, handlers, and createFiles. See the overview for how they compose.
---

The types and helpers that make up the plugin system. See the [overview](/docs/plugins) for how `wrap`, `extend`, and the pipeline fit together.

## FilesPlugin

The plugin object you pass to `plugins`. A `name` plus four optional hooks - `wrap` and `extend`, which the [overview](/docs/plugins) covers, [`capabilities`](#capabilities), which narrows what the instance advertises, and [`event`](#event), which maps storage events for its callers.

<AutoTypeTable
  path="../../packages/files-sdk/src/index.ts"
  name="FilesPlugin"
/>

### capabilities

```ts
capabilities?: (caps: AdapterCapabilities) => AdapterCapabilities;
```

Narrows what the instance reports through [`files.capabilities`](/docs/capabilities). The getter starts from the adapter's snapshot and folds every installed plugin's `capabilities` hook over it in `plugins` order, so each hook receives the snapshot as the earlier plugins left it. [`files.readonly()`](/docs/readonly) clones carry the plugins, so they report the same result.

Use it to advertise what your plugin refuses, so callers and the [UI gateway](/docs/ui/server/gateway) (which picks redirect vs. proxy, and presign vs. proxy, from `capabilities`) choose a path that works instead of hitting a fail-closed throw. The bundled body-transforming plugins do this: [`encryption()`](/docs/plugins/encryption) and [`compression()`](/docs/plugins/compression) report `signedUrl.supported`, `signedUpload.supported`, and `rangeRead` as `false` (a presigned URL or a byte range would serve the stored, transformed bytes, and a direct upload would skip the transform) along with `resumable` (they refuse resumable uploads), [`dedup()`](/docs/plugins/dedup) reports `signedUrl.supported`, `signedUpload.supported`, and every `conditional` flag as `false`, and [`validation()`](/docs/plugins/validation) and [`contentType()`](/docs/plugins/content-type) report `signedUpload.supported` as `false`. The plugins that veto [conditional operations](/docs/conditional-operations) clear exactly the flags they refuse: [`tiering()`](/docs/plugins/tiering) and [`failover()`](/docs/plugins/failover) every `conditional` flag, [`versioning()`](/docs/plugins/versioning) all but `exactRead`, and [`softDelete()`](/docs/plugins/soft-delete) `conditional.delete`. `tiering()` and `failover()` also report the intersection of their backends' capabilities, and [`signedUrlPolicy()`](/docs/plugins/signed-url-policy#capabilities-under-the-policy) turns off what its forced options rule out.

```ts lineNumbers
import { handlers, type FilesPlugin } from "files-sdk";

const watermark: FilesPlugin = {
  name: "watermark",
  // A presigned URL would skip the watermark, so don't advertise one.
  capabilities: (caps) => ({
    ...caps,
    signedUrl: { expiry: "none", supported: false },
  }),
  wrap: handlers({
    download: async (op, next) => addWatermark(await next(op)),
  }),
};
```

Return a new object rather than mutating the argument, and only narrow. The hook changes what is advertised, not what the adapter can do, so turning on a flag the core gates on doesn't make that option work.

The core also gates on the narrowed snapshot. A `range`, `metadata`, `cacheControl`, `control`, or `delimiter` that a plugin turns off, and an `expiresIn` on `url()` once `signedUrl.supported` is off, is refused before any plugin's `wrap` runs, with an error naming the plugin, so an outer plugin never does I/O for a call that's about to be rejected. That holds for a plugin that declares only `capabilities`, with no `wrap`, too.

### event

```ts
event?: (event: FileEvent) => FileEvent | null;
```

Maps a provider storage event from [`files-sdk/events`](/docs/events) into what this plugin's callers see. Return a rewritten event, the same one, or `null` to drop it.

A provider event describes what landed in the bucket, so a plugin that stores keys or bytes its callers never addressed has to translate it. The hook receives the event with the instance `prefix` already stripped, and hooks fold innermost first: `plugins` order reversed, the way stored bytes travel back out. The bundled plugins use it like this:

- [`dedup()`](/docs/plugins/dedup) drops its content-addressed blobs. It keeps `size` and `etag` on events for non-empty objects and clears them only on empty or unsized ones, which may be its pointers.
- [`encryption()`](/docs/plugins/encryption) and [`compression()`](/docs/plugins/compression) clear `size` on every provider event: a storage event carries no metadata, so the plugin can't tell its own objects (whose stored size isn't the caller's) from plain ones.
- [`versioning()`](/docs/plugins/versioning) and [`softDelete()`](/docs/plugins/soft-delete) drop events under their own prefixes, so a soft delete reads as one `deleted`.
- [`tiering()`](/docs/plugins/tiering) keeps events for keys routed to the hot tier. With `fallback: true` it throws, since a hot-tier delete may be a move to cold.

```ts lineNumbers
const thumbnails: FilesPlugin = {
  name: "thumbnails",
  // Generated thumbnails aren't the caller's files; don't report them.
  event: (event) => (event.key.startsWith(".thumbs/") ? null : event),
  wrap: handlers({/* … */}),
};
```

Events from a [gateway](/docs/ui/server/upload-lifecycle) upload or from the instance's own writes (`events({ sdk: true })`) are already caller-facing and skip the hook.

## FilesOperation

The discriminated union handed to `wrap` - one family per public verb, carrying the caller-facing inputs. Native [conditional operations](/docs/conditional-operations) stay in the existing `upload`, `download`, `delete`, and `copy` families and add a `mode` plus their ETag predicate. The array forms of `upload` / `download` / `head` / `exists` / `delete` set `bulk: true` on each fanned-out item; conditional modes are single-key only.

<AutoTypeTable
  path="../../packages/files-sdk/src/index.ts"
  name="FilesOperation"
/>

## handlers

```ts
function handlers(map: PluginHandlers): NonNullable<FilesPlugin["wrap"]>;
```

Builds a [`wrap`](/docs/plugins#wrap-intercepting-operations) from a per-verb map. Each handler is typed to its own operation and a same-kind `next`; verbs absent from the map pass through untouched.

An `upload` handler therefore sees ordinary uploads (no `mode`) as well as conditional creates and replaces (`mode: "create" | "replace"`); the same holds for `download` (`"exact"`), `delete` (`"match"`), and `copy` (`"conditional"`). Ordinary operations carry no `mode` at all, so branch on `isConditionalOperation(op)` or on the conditional literals, never on an "overwrite" value. Existing body-transforming handlers cover conditional writes automatically. The engine preserves a conditional root's family, mode, and predicate across every `next()` call and requires exactly one native operation before success.

## rejectConditional

```ts
function rejectConditional(
  op: Pick<ConditionalFilesOperation, "kind">,
  plugin: string,
  reason: string
): never;
```

Veto a [conditional operation](/docs/conditional-operations) from inside `wrap`, before any provider I/O, by throwing an `Unsupported` `FilesError` worded `<plugin>: conditional <kind> is unsupported because <reason>`. Use it for the modes your plugin cannot make atomic because of a side effect of its own — a snapshot, a mirror write, a pointer rewrite — that would sit outside the native compare-and-set. Every bundled plugin that vetoes uses this shape.

```ts lineNumbers
import { isConditionalOperation, rejectConditional } from "files-sdk";

const mirror: FilesPlugin = {
  name: "mirror",
  wrap: (op, next) => {
    if (isConditionalOperation(op)) {
      rejectConditional(
        op,
        "mirror",
        "the mirror write cannot share one native compare-and-set"
      );
    }
    return next(op);
  },
};
```

## createFiles

```ts
function createFiles<A extends Adapter, const P extends readonly FilesPlugin[]>(
  opts: FilesOptions<A> & { plugins?: P }
): Files<A> & ExtensionsOf<P>;
```

Constructs a `Files` instance whose type includes every plugin's [`extend`](/docs/plugins#extend-new-methods) surface. Runtime-identical to `new Files(opts)` - it exists only to surface the added methods on the type.
