Skip to content
Strife Docs

Reference

Versions and History

Every publish leaves behind a Published Version — a stamped RavenDB revision of the document. This is the row model, the three endpoints, and the locale-selective restore that both versions and snapshots share.

Strife keeps one Published Version for every publish of a content document: a RavenDB revision of the canonical document, stamped with who published it and when. Together with the current draft, any scheduled publish and any active Snapshot, Published Versions make up History — the per-document timeline the Studio shows in its History drawer.

This page documents the version model and the three endpoints behind it. If you are looking for the customer-site snapshot contract (the /snapshot/{token} route, the content-index rows, the SDK middleware), see Snapshots — versions and snapshots are restored through the same mechanism, and History can freeze a version into a snapshot for review (see Creating a snapshot from a published version), but a version itself has no URL and nothing about it is served on your site.

The stamp

Publishing a document — a batch publish, a scheduled publish, an MCP publish_content call, or the CLI seed that creates a published canonical directly — writes two fields onto the canonical document:

FieldValue
__publishedBy
The id of the user who published (the scheduling user for a scheduled publish, the CLI user for a seed)
__publishedAt
UTC round-trip timestamp ("o" format) of the publish

Immediately after that write is saved, Strife forces a RavenDB revision of the document — one per publish, holding the exact state that just went live, stamp included. A Published Version is a revision that carries this stamp. No revisions configuration is enabled on team databases; the stamped revision exists only because publish forces it.

Two things follow:

  • Only publishes create versions. Archive, soft-delete, unpublish (the restore-to-draft transition), copy, and the hand-crafted POST /content/{activeTeam} all write the canonical but stamp nothing and force no revision — they never appear as rows. So the newest RavenDB revision is not always the live canonical: a document can be written after its last publish without a new version existing for that write.
  • The stamp never reaches a draft, scheduled sibling, or snapshot. It is stripped when a draft is created or refreshed from a published canonical, protected from JSON Patch (alongside __dirtyLocales), and stripped from an incoming document POST body and after a copy. A stamp exists only on a published canonical and its revisions — never on a working copy, and never one a client could forge.

The three endpoints

All three live under /content, require team membership and the EditContent capability, and take {activeTeam} from the route — never the ActiveTeam claim. None are gated by a Studio feature flag; the History drawer's flags (below) only control whether the Studio renders the drawer.

GET /content/versions/{activeTeam}

The published rows for a canonical, newest first, paged.

Query paramRequiredNotes
id
yes
The canonical document id. A /draft, /scheduled or /snapshot/{token} id is rejected.
start
no
Raw revision offset to page from. Default 0.
pageSize
no
Default 25, capped at 100.
GET /content/versions/my-team?id=blog-post-1&start=0&pageSize=25
JSON
{
  "rows": [
    {
      "version": "AAAAAAAAxA0BAAAAAAAAAAI",
      "publishedAt": "2026-09-08T14:03:21.0000000Z",
      "publishedBy": "user-42",
      "locales": ["en", "sv"],
      "status": "published"
    },
    {
      "version": "AAAAAAAAwQ0BAAAAAAAAAAE",
      "publishedAt": "2026-09-01T09:12:44.0000000Z",
      "publishedBy": "user-7",
      "locales": ["en"],
      "status": "published"
    }
  ],
  "hasMore": true,
  "nextStart": 25
}

version is an opaque reference — a base64url encoding of the revision's RavenDB change vector, which is not path-safe on its own. Treat it as a token: pass it back verbatim to the body endpoint or to apply-version; never parse or construct one client-side.

nextStart is the raw revision offset consumed, not the number of rows returned — a page that skips an unstamped revision (there should never be one, but the read is defensive) still advances nextStart past it, so paging never desynchronizes. Pass it back as the next request's start. hasMore is true when more revisions exist beyond the page.

An id with no revisions returns an empty rows array, not a 404.

Errors ({ "error": "...", "message": "..." }):

StatuserrorWhen
400
missing_id
No id given
400
sibling_id
id names a /draft, /scheduled or /snapshot/{token} sibling
400
not_content
id exists but is not in a content collection
400
bad_paging
start is negative
404
not_found
The canonical does not exist, or is soft-deleted

GET /content/version/{activeTeam}

One version body, for preview.

GET /content/version/my-team?id=blog-post-1&version=AAAAAAAAxA0BAAAAAAAAAAI

Returns the revision body as stored, with @metadata and the client-injected Id property stripped and id set to the canonical id — the same shape a live document has when loaded.

Errors:

StatuserrorWhen
400
missing_id / sibling_id / not_content
Same as the list endpoint
400
bad_reference
version is not a well-formed reference
404
not_found
The canonical is missing or soft-deleted, or the reference does not name a stamped revision of that canonical (including a reference belonging to another document's revision)

The refusal never distinguishes "wrong document" from "not a version" — a reference cannot be used to probe other documents.

POST /content/apply-version/{activeTeam}

Restores a History row — a snapshot or a published version — into the working copy. This is the one restore operation both PreviewAction's snapshot restore and the History drawer use.

JSON
{
  "documentId": "blog-post-1",
  "kind": "revision",
  "version": "AAAAAAAAxA0BAAAAAAAAAAI",
  "locales": ["en"]
}
FieldTypeNotes
kind
"snapshot" | "revision"
Which source to restore from
documentId
string
The canonical id. Required for kind: "revision"; optional for kind: "snapshot" (when given, must match the snapshot's canonical)
snapshotId
string
The active snapshot's id ({canonical}/snapshot/{token}), when kind: "snapshot"
version
string
The opaque reference from a versions row, when kind: "revision"
locales
string[] | null
null restores every locale the source carries; a non-empty subset of the team's configured locales restores only those (see Restore semantics)

Response mirrors apply-snapshot: a one-item batch-result list.

JSON
[
  { "id": "blog-post-1", "status": 0, "document": { "id": "blog-post-1/draft", "...": "..." } }
]

A failure reports status: 1 with a reason:

JSON
[
  { "id": "blog-post-1", "status": 1, "reason": "The draft changed while restoring. Save pending edits and try again." }
]

Errors. Three body shapes share the 400 status, so branch on the shape, not the status code:

  • Problem details — { "title": "...", "status": 400, "errors": { ... } } (application/problem+json), from the framework's request validation. Only a missing, empty, or literal-null JSON body gets this.
  • Object — { "error": "...", "message": "..." }, the same shape the version reads use. This is the endpoint refusing the request before it reaches the store; there is no batch row and no reason.
  • Batch list — the one-item batch-result list shown above, with status: 1 and a reason. Every refusal that involves the document, snapshot, or the team's configuration comes back this way.
StatusShapeerror / reasonWhen
400
problem details
—
No JSON body, or a literal null body
400
object
bad_kind
kind is not "snapshot" or "revision"
400
object
missing_id
kind: "revision" without a documentId
400
object
missing_version
kind: "revision" without a version
400
object
missing_snapshot_id
kind: "snapshot" without a snapshotId
400
object
mismatch
kind: "snapshot" with a documentId that is not the snapshot's canonical
400
batch list
reason text
A sibling id where a canonical is expected, a malformed reference, an empty locales list, a locales list with more than 64 entries (refused as Too many locales in request., nothing echoed), or a locale outside the team's configured set (the reason names at most five of the unknown codes)
403
batch list
reason text
Restoring into the draft of a published document without the right to change live content
404
batch list
reason text
Unknown document or snapshot, or a version that does not name a published revision of documentId
409
batch list
reason text
The snapshot is expired/revoked, or the draft (or its activity document) changed underneath the restore — retry once after re-reading

The canonical document is never written by this endpoint; only the draft (and its activity document) change.

Restore semantics

apply-version and apply-snapshot (with its own now-optional locales, see Snapshots) share one locale-selective merge:

  • No locales list — the whole draft is replaced from the source, exactly as snapshot restore always worked: every locale the source carries overwrites the draft, __dirtyLocales becomes the source's full locale set, and any locale published since the source was taken (and therefore absent from it) is reverted from the live canonical rather than left stale.
  • A locales list — only those locales come from the source. Every other configured locale the draft already carried is put back byte-identical to what it was immediately before the restore (never re-derived from the canonical, so an in-progress, unpublished translation is never disturbed). A locale neither the source nor the pre-restore draft carried is reverted from the canonical, same as the no-list case. Non-localizable fields always come from the source, regardless of which locales were selected. __dirtyLocales becomes the union of the pre-restore dirty set and the restored locales — restored locales are marked as having unpublished changes; everything else keeps whatever mark it already had.
  • Activity — restoring appends one APPLY_REVISION (or APPLY_SNAPSHOT) entry to the document's activity log naming the source, and prunes prior EDIT entries for the restored locales only; a whole-draft restore also prunes locale-less entries.
  • No draft — restoring creates one from the source.
  • Concurrency — the write runs under optimistic concurrency over the draft and its activity document together, so it lands atomically or not at all; a 409 leaves no partial state, and the caller re-reads and retries with the same locale list.
  • Restoring is never publishing — the draft is replaced, but nothing goes live until the normal publish flow runs.

Permissions

Both reads and the restore require team membership and the EditContent capability. Restoring into the draft of a published document additionally requires the right to change live content (ChangeLiveContent) — the same rule snapshot restore has always had. A caller without it can still read and preview every row; the restore call itself is refused with 403.

The Studio flags

The History drawer in Studio is gated by the history feature flag. The snapshots flag decides what is in it: with both on, the drawer also lists the document's snapshots and offers Share on every row; with history alone, the drawer shows versions only — the draft, the published version and every previous version — and nothing about snapshots. With history off, none of this is exposed in the UI, and snapshots does nothing on its own: the pre-History snapshot popover is no longer rendered. Neither flag gates these three endpoints or the snapshot endpoints — they answer the same way regardless of Studio's flag state.

Retention and erasure

No retention limit is configured: Published Versions are kept indefinitely, as far back as RavenDB holds revisions for the document. A future retention setting (a MinimumRevisionsToKeep on the revisions configuration) would trim them, but none exists today.

Soft-deleting a document does not delete its revisions — RavenDB keeps them — but both endpoints refuse to serve versions of a soft-deleted canonical (404). To actually remove a document's history, an operator runs DeleteRevisionsOperation against its canonical id, or destroys the whole team database.

What does not create a version

  • Archiving a document
  • Soft-deleting a document
  • Unpublishing (the restore-to-draft transition)
  • Copying a document
  • The hand-crafted POST /content/{activeTeam} document create/update
  • Discarding a draft
  • Creating, renaming or revoking a snapshot

Only a publish — through the Studio batch publish, a scheduled publish, MCP publish_content, or the CLI seed — stamps the canonical and records a version.

Two publishes within seconds

The version is forced after the publish has saved, and that save can block for up to 15 seconds on index and replication waits. If a second publish of the same document commits inside that window, the first publish finds a __publishedAt it did not write and records nothing: the second publish records its own version, and the first one is not a row. The first publish's content was live only for those seconds. Each such case counts as outcome=superseded on strife.api.publish.revisions (or strife.worker.publish.revisions for a scheduled publish), next to ok and error.