Concepts
Revisions and concurrency
Immutable entry snapshots, derived statuses, optimistic concurrency, and restore semantics.
Every explicit save of an entry creates a revision: an immutable, complete snapshot of the entry's data at that moment. Revisions are never diffs and never mutated — updates append a new revision and move the entry's draft pointer. This is what makes agent output reviewable: every change is attributable, comparable, and restorable.
The revision record
{
"id": "rev_01...",
"entryId": "ent_01...",
"revisionNumber": 4,
"collectionVersionId": "cver_01...",
"data": { "title": "Launch day", "body": "..." },
"createdByType": "agent",
"createdById": "key_01...",
"changeSummary": "Tightened the intro paragraph",
"createdAt": "2026-07-23T09:41:00.000Z"
}
revisionNumberincreases from 1 per entry and is unique within the entry.datais the full field document, validated against the deployed schema at write time.collectionVersionIdbinds the revision to the schema version it was written under. Historical revisions stay bound to their original schema version even after later schema pushes; only current heads are re-validated when schemas change or when a change set publishes.changeSummaryis an optional caller-supplied note (max 500 characters) passed on create and update requests.
Attribution
createdByType records who produced the snapshot:
| Actor type | Meaning |
|---|---|
user |
A dashboard session |
api_key |
A scoped API key (SDK, CLI, or direct HTTP) |
agent |
An agent acting through the MCP server or an agent-attributed credential |
system |
Myna itself (migrations, background jobs) |
createdById holds the concrete user or key id when applicable.
Entry pointers and derived status
An entry carries three revision pointers: draftRevisionId (latest saved snapshot), publishedRevisionId (what the public content API serves), and lastPublishedRevisionId (the most recently published snapshot, retained across unpublish). Status is never stored — it is derived from the pointers:
| Status | Condition |
|---|---|
deleted |
deletedAt is set |
draft |
No published revision and never published |
unpublished |
No published revision, but a last-published revision exists |
published |
Draft and published pointers are identical |
changed |
Published, with newer draft edits on top |
Publishing a change set points publishedRevisionId (and lastPublishedRevisionId and draftRevisionId) at the validated target revision. Later edits only advance draftRevisionId, moving the entry to changed.
Optimistic concurrency
Concurrent writers to the same change set are detected, not merged. PATCH /v1/projects/:project/entries/:entry accepts expectedRevisionId — the revision the client based its edit on. If it no longer matches the revision the write would actually build on, the write fails with STALE_REVISION (HTTP 409):
{
"type": "https://myna.sh/errors/stale-revision",
"title": "Stale revision",
"status": 409,
"code": "STALE_REVISION",
"detail": "The entry was modified by another writer.",
"expectedRevisionId": "rev_01A...",
"currentRevisionId": "rev_01B..."
}
Re-read the entry, rebase the edit, and retry.
What the edit builds on depends on the change set. With a changeSetId, it is that change set's own proposal for the entry — or what is published, if that change set has not touched the entry yet. Without one, it is the entry's draftRevisionId. An entry may sit in several open change sets, so "the current draft" is not a single revision, and an edit inside one change set never silently builds on another's unpublished work. Omitting expectedRevisionId performs a last-write-wins save. STALE_REVISION also appears as an item error code during publish validation when an entry was published after a change was staged (a stale base, settled by rebasing). See error codes.
Reading history
$ myna entries revisions posts/launch
# GET /v1/projects/:project/entries/:entry/revisions — newest first
# GET /v1/projects/:project/entries/:entry/revisions/:revision
Both require content:read. Revision retention is plan-dependent: 30 days on Free, indefinite on Pro — see plans.
Restore creates a new revision
Restoring never rewrites history. POST /v1/projects/:project/entries/:entry/revisions/:revision/restore (scope content:restore) copies the historical revision's data into a new revision with the next revision number and the change summary Restored from revision N. The new revision:
- becomes the entry's draft (
draftRevisionId), clearingdeletedAtif the entry was soft-deleted; - is bound to the collection's current schema version, not the historical one;
- is staged as a
createorupdateitem on a change set, so nothing reaches the live site until that change set publishes.
POST /v1/projects/:project/entries/:entry/restore is a shorthand that restores a soft-deleted entry from its last-published revision (falling back to its latest draft).
