The server compares. Your computer merges.
The server compares two version vectors, nothing else. It cannot read a sealed page, so it cannot merge one.
Eleven fields make a page.
A page travels as one record. key_id decides whether we can read it.
| Field | Meaning |
|---|---|
| id | uuid, the only stable identity on the server |
| path | Where the page sits. Ciphertext once key_id is not none |
| scope | The sealed space, by an id that says nothing; empty when open |
| key_id | Names a key, never holds one. none means readable here |
| version | A counter; in an open space, the server's concurrency guard |
| vclock | {deviceId: counter}, the concurrency truth for a sync write |
| content_b64 | The body. Opaque to us in a sealed space |
| content_hash | SHA-256 of the stored bytes; of ciphertext, in a sealed space |
| deleted | Tombstone flag |
| updated_at | Server clock, to the microsecond |
| seq | The write's number in commit order: the pull cursor |
none means an open space: markdown we index, search, and keep history of.
A real key means a sealed space: ciphertext, and only vectors for the server to compare.
The server's one decision is arithmetic.
Every page carries a version vector. A push lands only when its vector dominates the stored one.
Your computer has seen everything the server has.
200 acceptedEqual vectors advance nothing.
409 + server_vclockTwo computers edited offline. The server cannot merge ciphertext.
409 + server_vclock{laptop:5, desk:2}{laptop:4, desk:2}{laptop:4, desk:3}{laptop:4, desk:3}{laptop:5, desk:2}{laptop:5, desk:3}{laptop:5, desk:4}{laptop:5, desk:2}The server decides whether. Your computer decides what.
Refusals name the field, never the value.
The refusal says which field failed and why. Echoing the value would leak it, and a test forbids that.
In an open space the brain refuses a path over 512 characters, a body over 1,000,000 characters, or a NUL in either. A sealed page's path travels as at most 1024 characters of ciphertext, from a bucketed path of about 500 bytes.
Four functions close any fork.
The vector arithmetic ships in @instibrain/protocol, which the server and
the sync client both import.
| Function | What it does |
|---|---|
| join(a, b) | Gives the vector a merged page carries |
| bump(v, deviceId) | Counts one local edit, on your device's counter |
| concurrent(a, b) | Names a real fork |
| isValidVClock(v) | Refuses a shape that would poison the ordering |
Fetch, decrypt both, merge, join, bump, push again. None of it
happens on our side.
HTTP for a client you write, MCP for an AI, one contract.
The sync client calls the routes, and your AI calls the tools. Every tool is on the tool reference.
| Call | What it does |
|---|---|
| POST /v0/nodes | Mints a uuid. No row is written. |
| GET /v0/nodes | Everything after your seq cursor, 500 at a time, in commit order |
| GET /v0/nodes/:id | One page: the server's copy, for a merge |
| PUT /v0/nodes/:id | 200 when your vector dominates; otherwise 409 with the server's vector, or a named code |
| search_brain | Open spaces only. Up to 500 characters in, the first 20 words counted; paths and the matching paragraph out. |
| read_node | One open page by path, as markdown |
| write_node, append_node, patch_node, move_node, delete_node | Edit an open space. The server reads it, so expected_version guards instead of a vector. |
A client follows six rules.
Skip the second, and your client re-reads one page forever.
- Mint the page id locally, or call POST /v0/nodes.
- Carry the cursor as the seq of the last record you took.
- Keep a stable deviceId; bump it on every local edit.
- On 409 concurrent: fetch, merge, join, bump, retry.
- Validate against the contract package before sending.
- Never expect the server to resolve a sealed fork. It refuses.
Every write needs a credential, held to its person's role. Who reads a sealed space is signed, in its chain.
A sync clash never loses a word: your computer merges it, not our server.
The keys and their limits are on the security page.