---
title: versioning
description: Snapshot an object's prior bytes before every overwrite or delete, and roll a key back with versions() and restore(). Server-side copies under a hidden prefix - body-transparent, no native dependencies, works on any adapter.
---

The built-in `versioning()` plugin keeps a history of every object. Before an [`upload`](/api/upload), [`delete`](/api/delete), or the destination of a [`copy`](/api/copy) / [`move`](/api/move) clobbers an existing object, it server-side-copies the current bytes to a time-stamped key under a hidden version prefix. Two new methods - `versions()` and `restore()` - let you list that history and roll a key back.

Unlike [`encryption()`](/plugins/encryption) and [`compression()`](/plugins/compression), it's **body-transparent**: it never buffers, transforms, or even reads the body, so streaming, [range downloads](/api/download), [`url()`](/api/url), and [`signedUploadUrl()`](/api/signed-upload-url) all keep working. It has **no native dependencies** and works on any adapter.

```ts lineNumbers
import { createFiles } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { versioning } from "files-sdk/versioning";

const files = createFiles({
  adapter: s3({ bucket: "uploads" }),
  plugins: [versioning({ limit: 10 })],
});

await files.upload("notes.txt", "v1");
await files.upload("notes.txt", "v2"); // "v1" snapshotted first

const [previous] = await files.versions("notes.txt");
await files.restore("notes.txt", previous.versionId); // back to "v1"
```

<Callout>
  `versions()` and `restore()` are contributed by the plugin's `extend`, so they
  only appear on the **type** when you construct with
  [`createFiles`](/plugins/api#createfiles) (identical to `new Files()` at
  runtime). `versioning()` is the first built-in plugin to use `extend`.
</Callout>

## How it works

Every snapshot is a plain object **copy**, not a re-upload:

1. Before a write would overwrite or delete a key, the plugin `head`s it. If nothing's there (a first write), there's nothing to snapshot and it moves on.
2. Otherwise it copies the current object to `"<prefix>/<key>/<versionId>"` - the default prefix is `.versions`, so a version of `photos/a.jpg` lands at `.versions/photos/a.jpg/<versionId>`.
3. The live write then proceeds as normal.

The `versionId` is the object's last-modified time (zero-padded so ids sort chronologically) plus a slug of its ETag, so versions list newest-first and stay unique per change.

Because snapshots are copies of whatever is **already stored**, the plugin composes cleanly with the transforming plugins: a version of an encrypted object is still encrypted (the wrapped key rides along in its metadata) and restores to readable plaintext.

## Restoring

`restore(key, versionId?)` copies a version back over the live key. Omit the `versionId` to restore the **newest** version - an undo of the last change:

```ts lineNumbers
await files.upload("report.pdf", v1);
await files.upload("report.pdf", v2); // overwrites; v1 is snapshotted

await files.restore("report.pdf"); // back to v1
```

A restore **snapshots the current bytes first**, so it's itself reversible - you can always roll forward again. It resolves to the restored [`StoredFile`](/api/stored-file). Restoring works after a delete too, since the delete was snapshotted:

```ts lineNumbers
await files.delete("report.pdf");
await files.restore("report.pdf"); // undeletes it
```

## Listing history

`versions(key)` returns the saved versions **newest-first**, each with the `versionId` you pass to `restore()`:

```ts lineNumbers
const history = await files.versions("report.pdf");
// [
//   { versionId, key: ".versions/report.pdf/…", size, lastModified, etag? },
//   …
// ]
```

The `key` on each entry is a real, downloadable object, so you can preview a version without restoring it: `await files.download(history[0].key)`.

## Capping history

By default history grows unbounded. Set `limit` to keep only the newest N versions per key - the oldest are pruned after each snapshot:

```ts
versioning({ limit: 20 });
```

## Choosing the prefix

Snapshots live under `.versions` by default. Override it with `prefix`, and keep your own data out of it:

```ts
versioning({ prefix: ".history" });
```

Objects under the version prefix are **hidden from [`list()`](/api/list)** so snapshots don't clutter your listings - unless you explicitly list within the prefix (which is how `versions()` reads them). Filtering preserves the page `cursor`, so [pagination](/api/list) still resumes correctly; pages may just come back shorter.

## Ordering

Versioning operates on logical keys and snapshots whatever the rest of the pipeline stored, so place it **first** (outermost):

```ts
plugins: [versioning(), compression(), encryption(key)];
```

## Things to keep in mind

- **A `head` + `copy` per overwrite/delete.** Snapshotting adds two adapter round-trips to writes that hit an existing object; first writes cost only the `head`. It's the price of keeping history.
- **Direct presigned writes bypass it.** A client `PUT` to a [`signedUploadUrl`](/api/signed-upload-url) never runs the plugin, so no snapshot is taken. Write through the instance to version. It's a safety net, not a security control, so - unlike [`validation()`](/plugins/validation) - it doesn't fail closed.
- **`move` snapshots only its destination.** A rename relocates the bytes rather than destroying them, so the source isn't snapshotted; the data lives on at the new key.
- **History is unbounded** unless you set `limit`.
- **Don't store your own data under the version prefix.** Writes there are passed through un-versioned and hidden from `list()`.
