# Versions

> Numbered snapshots of a subject's content, with JSON Patch diffs, restore, debounced auto snapshots, frozen versions and retention.

Source: https://bettersupabase.com/docs/blocks/versions

`versions` keeps numbered snapshots of any subject: a document, a quote, a board. Each version holds JSON content (or a Storage path), a SHA-256 content hash, an optional label and where it came from (`manual`, `auto` or `restore`). Reading versions needs `versions.read`, taking and labeling one needs `versions.create`, restoring needs `versions.restore` and deleting needs `versions.manage`. Every call also checks the subject's own read permission, and a restore checks its `write` permission from [`config.subjects`](/docs/blocks/sql#subject-registry).

```bash
better-supabase sql add versions
```

```ts
import { createVersions } from "better-supabase/blocks/versions";

const versions = createVersions({ transport });
const v = await versions
  .snapshot({
    organizationId,
    subjectType: "quote",
    subjectId,
    label: "Sent to customer",
  })
  .orThrow();
const patch = await versions.diff(previousId, v.id).orThrow();
await versions.restore(previousId).orThrow();
```

A snapshot whose content matches the latest version is not written again: `snapshot` returns the latest version with `unchanged: true`. Versions are numbered per subject from 1.

## Configure subjects [#configure-subjects]

Each subject type lists the `columns` a snapshot copies and a restore writes back. A type with an `apply` function restores through it instead: the function takes `(subject_id text, content jsonb)`.

```ts title="better-supabase.config.ts"
subjects: {
  quote: { table: "public.quotes", idType: "uuid", permissions: { read: "quotes.read", write: "quotes.update" } },
  page: { table: "public.pages", idType: "uuid" },
},
sql: {
  modules: {
    versions: {
      options: {
        keep: 50,
        debounce: 300,
        subjects: {
          quote: {
            columns: ["title", "lines", "total"],
            freeze: { column: "status", values: ["sent", "accepted"] },
          },
          page: { columns: ["body"], auto: true, apply: "public.apply_page_version" },
        },
      },
    },
  },
},
```

| Option                    | Default | What it does                                                                                          |
| ------------------------- | ------- | ----------------------------------------------------------------------------------------------------- |
| `subjects.<type>.columns` | none    | The columns a snapshot without `content` copies, and a restore writes back                            |
| `subjects.<type>.apply`   | none    | A function `(subject_id text, content jsonb)` that restores a version in place of `columns`           |
| `subjects.<type>.auto`    | `false` | Snapshot on every insert or change to `columns`                                                       |
| `subjects.<type>.freeze`  | none    | `{ column, values }`: when `column` changes to one of `values`, keep a frozen version labeled with it |
| `keep`                    | `50`    | How many versions without a label or freeze each subject keeps                                        |
| `debounce`                | `300`   | Seconds an auto snapshot replaces the previous auto snapshot's content instead of adding a version    |

A frozen version can't be relabeled or deleted, so a quote keeps what was sent. A label keeps a version out of retention; `label(id, null)` removes it.

## Diff [#diff]

`diff(fromId, toId)` returns the [RFC 6902](https://www.rfc-editor.org/rfc/rfc6902) JSON Patch that turns one version's content into the other's. `createPatch(from, to)` and `applyPatch(document, patch)` work on any JSON value, and `applyPatch` throws a `JsonPatchError` with the failing operation's `index` for an operation the RFC calls an error. Object members are compared by name and array elements by position.

## Restore [#restore]

`restore(id)` writes the version back to the subject through its `apply` function or `columns`, then records the result as a new version with source `restore`. The auto and freeze triggers skip the write a restore makes. A version kept in Storage (`storagePath` without content) is restored by the app.

## Share a version [#share-a-version]

A [share link](/docs/blocks/share-links) to a subject can point at one version: keep the version id in the link's metadata and read it with `get(id)`, which returns the content.

## Events and hooks [#events-and-hooks]

The module records `version.created`, `version.restored` and `version.deleted`, each with `organizationId`, `subjectType` and `subjectId`, so the [activity and watcher sinks](/docs/extending/events) handle them. It calls `before_version_snapshot`, `after_version_snapshot`, `before_version_restore` and `after_version_restore` around its writes.

## With ReUI [#with-reui]

The client has no UI dependency. A history panel lists `list()`, shows `diff()` between two rows, and calls `restore()` from a ReUI timeline or table:

```tsx
const rows = await versions.list(organizationId, "quote", quoteId).orThrow();
// <Timeline> from ReUI: one item per row, labeled with row.label ?? `Version ${row.number}`
```