# Named revisions

A revision is a **manual save point**: a named, immutable copy of a document's
content at the moment it was captured. Where [History & replay](history.md)
gives you every keystroke (the append-only patch log), revisions give you the
handful of milestones a human actually cares about ("First draft", "Sent to
legal", "v2.0") and a one-call way to roll the live document back to one of
them.

Revisions are a server feature exposed over REST; they need no editor plugin
support. Any client with the `write` action can create and restore them; any
client with `read` can list and view them.

## The model

```ts
interface RevisionMeta {
	id: string; // server-assigned (UUID)
	name: string; // the human label you supplied
	seq: number; // document sequence number the snapshot was taken at
	authorId: string; // who captured it (from the resolved identity)
	authorName: string;
	createdAt: number; // epoch ms
}

interface Revision extends RevisionMeta {
	snapshot: SerializedElementNode; // the full captured document
}
```

The snapshot is the **live in-memory document** at capture time. It includes
edits that have not yet been folded into a storage snapshot, so a revision is
always faithful to what participants currently see, down to the last
character.

Revisions are stored via the same [storage adapter](storage.md) as everything
else (a `collab_revisions` table under Postgres; an in-memory map under the
default adapter) and are erased with the document on
[right-to-erasure delete](data-lifecycle.md).

## Endpoints

All paths are under `ROUTE_PREFIX` (default `/collab`; see
[Configuration](configuration.md)) and use the **same authentication as the WS
handshake**: `Authorization: Bearer <token>` or `?token=<token>`, guests only
in anonymous mode.

### Create: `POST /docs/:docId/revisions`

Captures the live document as a named revision. Requires the `write` action.

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"First draft"}' \
  https://collab.example.com/collab/docs/report-42/revisions
```

Response (`200`) contains the metadata, without the snapshot payload:

```json
{
	"success": true,
	"revision": {
		"id": "3f2a…",
		"name": "First draft",
		"seq": 128,
		"authorId": "alice",
		"authorName": "Alice",
		"createdAt": 1751835600000
	}
}
```

An empty or whitespace-only `name` is rejected with `400`
(`{"error":{"code":"bad_request"}}`). Names are trimmed and capped at 200
characters.

### List: `GET /docs/:docId/revisions`

Returns revision **metadata only** (no snapshots), newest first. Requires
`read`.

```json
{
	"success": true,
	"revisions": [
		{
			"id": "9c1…",
			"name": "Sent to legal",
			"seq": 240,
			"authorName": "Bob",
			"createdAt": 1751922000000,
			"authorId": "bob"
		},
		{
			"id": "3f2…",
			"name": "First draft",
			"seq": 128,
			"authorName": "Alice",
			"createdAt": 1751835600000,
			"authorId": "alice"
		}
	]
}
```

### View as HTML: `GET /docs/:docId/revisions/:revId/html`

Serializes the revision's captured snapshot to an HTML string (same serializer
and escaping rules as the [current-document HTML export](history.md#export-the-current-document-as-html)).
Requires `read`. Unknown revision → `404` (`revision_not_found`).

```bash
curl -H "Authorization: Bearer $TOKEN" \
  https://collab.example.com/collab/docs/report-42/revisions/3f2a…/html \
  > first-draft.html
```

### Restore: `POST /docs/:docId/revisions/:revId/restore`

Replaces the **live** document's content with the revision's snapshot.
Requires `write`.

A restore is **not** a silent state swap. It is applied as an
ordinary sequenced edit:

- every connected participant receives it as a normal patch stream and their
  editor converges on the restored content live, no reload;
- it lands in the history log like any other change, so a restore is itself
  reversible (restore an earlier revision, or replay history);
- it therefore respects the same OT ordering as concurrent typing.

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  https://collab.example.com/collab/docs/report-42/revisions/3f2a…/restore
```

Response: `{"success":true,"seq":<new head seq>}`.

> **Note: How restore is expressed**
>
> The server builds one batch that removes every current top-level node and
> re-inserts the revision's nodes (re-id'd with fresh ids so the inserts are
> guaranteed to materialize), then submits it through the normal sequencer as
> a `system` edit. There is no special "reset" opcode; restore is just
> patches, which is why it converges and replays like everything else.

> **Warning: Comment anchors and restore**
>
> Comment anchors live inside the document HTML
> ([see the protocol](protocol.md#comments)). When a restore removes text
> that a comment was anchored to, that thread becomes **orphaned**: it
> survives (threads are stored server-side) but shows as unanchored in the
> comments panel, exactly as when a user deletes the anchored text by hand.
> Restoring does not delete comments.

## Error responses

| Status | Code                            | When                                                        |
| ------ | ------------------------------- | ----------------------------------------------------------- |
| `400`  | `bad_request`                   | Create with an empty `name`.                                |
| `401`  | `auth_required` / `auth_failed` | No/invalid token (see [Authentication](authentication.md)). |
| `403`  | `forbidden`                     | Create/restore without the `write` action.                  |
| `404`  | `revision_not_found`            | View/restore an unknown revision id.                        |
| `500`  | `internal`                      | Storage failure.                                            |

## When to use revisions vs. history

| You want…                                       | Use                                    |
| ----------------------------------------------- | -------------------------------------- |
| Named milestones a person chose ("v1", "final") | Revisions                              |
| Every keystroke / a scrubbable timeline         | [History](history.md)                  |
| One-click rollback broadcast to everyone live   | Revisions (restore)                    |
| The document as of an arbitrary `seq`           | [History](history.md) (`?from=N&to=N`) |

Revisions and history are complementary: revisions are the sparse,
human-curated layer on top of the dense, automatic history log.
