Why ids and paths
A page is an array of components. Each has a stableid (assigned once, never changed), a type and props:
props.items.1.name inside m81xq0ze identifies one value even if two logos share a name. Array items are addressed by index, so a reorder is a patch on the array.
Articles and forms use the same model: their body is a component stream. Row-level fields (title, date) use a reserved meta id, so a patch can target the row itself.
Saving: compare-and-swap
Every page, article and form row has aversion integer plus updated_by and updated_by_name. A save sends the working state along with the version it was based on. The update is conditional:
Reconciling: three-way merge
On a miss the editor fetches the current row (theirs) and merges its working state (ours) against the snapshot it loaded (base):- Components merge by id, so two people editing different sections both keep their work.
- Fields inside a component merge when only one side changed them.
- A field both sides changed differently is a conflict. The save throws a
WriteConflictErrorcarrying theirs, base, ours and the conflicting paths. The admin reports it rather than guess. What the user sees is in Guides: Edit mode.
Drafts
An unpublished article or form saves straight to its row. A published one autosaves to a shadow drafts table, so builds only ever ship published content; Publish folds the shadow into the live columns. Scheduling is apublish_at column plus an embargo in the public read policy.
Presence
While a page is open the editor joins a realtime channel for it. Peers appear in the top bar with the field they are on. Presence is awareness, not locking; the version check keeps writes safe.Where it lives
All in the SDK, framework-agnostic:
In production the admin’s saves route through the API’s content routes, which reconstruct the same conflict contract as a
409.