> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stage.systems/llms.txt
> Use this file to discover all available pages before exploring further.

# The write model

> Content is addressed by a stable id and a dotted path. Saves are compare-and-swap on a version counter with a three-way merge on conflict.

Every edit is a **patch**: a component id, a dotted path to a field, and a new value. Nothing matches on text.

```json theme={null}
{ "id": "k3f9a2bq", "path": "props.title", "value": "A better headline" }
```

## Why ids and paths

A page is an array of components. Each has a stable `id` (assigned once, never changed), a `type` and `props`:

```json theme={null}
{
  "components": [
    { "id": "k3f9a2bq", "type": "hero",  "props": { "title": "Hello", "image": "…" } },
    { "id": "m81xq0ze", "type": "logos", "props": { "items": [{ "name": "A" }, { "name": "B" }] } }
  ]
}
```

Rendered HTML carries the id on the wrapper and the path on each field. A click on `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 a `version` 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:

```sql theme={null}
update pages set …, version = :base + 1
where org_id = :org and slug = :slug and version = :base
```

If the row is still at the loaded version, the write lands. If someone saved first, the update matches nothing and the save reconciles instead of overwriting.

## 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 `WriteConflictError` carrying theirs, base, ours and the conflicting paths. The admin reports it rather than guess. What the user sees is in [Guides: Edit mode](/editing/edit-mode#two-people-at-once).

A successful merge retries against the new version.

## 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 a `publish_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:

| Concern                                            | Module                                                             |
| -------------------------------------------------- | ------------------------------------------------------------------ |
| Patches, id lookup, dotted paths, id assignment    | `ids.ts` (`applyPatch`, `findById`, `setByPath`, `ensureIds`)      |
| Versioned saves for pages, articles, forms, drafts | `save.ts` (`savePageCAS`, `saveDocumentCAS`, `WriteConflictError`) |
| Merge and conflict detection                       | `merge.ts` (`threeWayMerge`, `detectConflicts`)                    |
| Presence                                           | `presence.ts`                                                      |

In production the admin's saves route through the API's content routes, which reconstruct the same conflict contract as a `409`.
