instibrain
instibrain  /  how it works

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.

brain.instibrain.ai/v0 desk-2a
PUT /v0/nodes/7f3a4c-…-9d21 { "vclock": { "laptop": 4, "desk": 3 }, "content_b64": "k7Qm2bR1x9…" } 409 { "code": "concurrent", "message": "…", "server_vclock": { "laptop": 5, "desk": 2 } } # neither vector dominates. the server holds # ciphertext, so it stops here and says so. # decrypt both, merge, join, bump, push again 200 { "ok": true, "id": "7f3a4c-…-9d21" }
one 409, start to finish
the record

Eleven fields make a page.

A page travels as one record. key_id decides whether we can read it.

FieldMeaning
iduuid, the only stable identity on the server
pathWhere the page sits. Ciphertext once key_id is not none
scopeThe sealed space, by an id that says nothing; empty when open
key_idNames a key, never holds one. none means readable here
versionA counter; in an open space, the server's concurrency guard
vclock{deviceId: counter}, the concurrency truth for a sync write
content_b64The body. Opaque to us in a sealed space
content_hashSHA-256 of the stored bytes; of ciphertext, in a sealed space
deletedTombstone flag
updated_atServer clock, to the microsecond
seqThe write's number in commit order: the pull cursor
what the two values of key_id mean

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 rule

The server's one decision is arithmetic.

Every page carries a version vector. A push lands only when its vector dominates the stored one.

case 01It dominates
pushlaptop 5desk 2
storedlaptop 4desk 2

Your computer has seen everything the server has.

200 accepted
case 02It is identical
pushlaptop 5desk 2
storedlaptop 5desk 2

Equal vectors advance nothing.

409 + server_vclock
case 03Neither dominates
pushlaptop 4desk 3
storedlaptop 5desk 2

Two computers edited offline. The server cannot merge ciphertext.

409 + server_vclock
1 · a write lands
laptopedits, pushes {laptop:5, desk:2}
server200 it dominates {laptop:4, desk:2}
deskedits offline {laptop:4, desk:3}
2 · a fork
deskpushes {laptop:4, desk:3}
server409 neither dominates. It returns {laptop:5, desk:2}
3 · the computer merges
serverwaits it holds ciphertext and cannot merge
deskdecrypts both, merges joins to {laptop:5, desk:3}
4 · the merge lands
deskbumps, pushes {laptop:5, desk:4}
server200 it dominates {laptop:5, desk:2}

The server decides whether. Your computer decides what.

the rejection

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.

PUT /v0/nodes/7f3a4c-…-9d21  ·  a path over the 1024-character limit
# sent { "path": "cGF0aC9jaXBoZXJ0ZXh0…1400 characters…", "vclock": { "laptop": 5 } } # returned — 400 { "code": "invalid_request", "message": "The brain did not accept that request.", "field": "path", "reason": "too long" } # the field. the reason. never the bytes.
the client's half

Four functions close any fork.

The vector arithmetic ships in @instibrain/protocol, which the server and the sync client both import.

FunctionWhat 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
the 409 path

Fetch, decrypt both, merge, join, bump, push again. None of it happens on our side.

the wire

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.

CallWhat it does
POST /v0/nodesMints a uuid. No row is written.
GET /v0/nodesEverything after your seq cursor, 500 at a time, in commit order
GET /v0/nodes/:idOne page: the server's copy, for a merge
PUT /v0/nodes/:id200 when your vector dominates; otherwise 409 with the server's vector, or a named code
search_brainOpen spaces only. Up to 500 characters in, the first 20 words counted; paths and the matching paragraph out.
read_nodeOne open page by path, as markdown
write_node, append_node, patch_node, move_node, delete_nodeEdit an open space. The server reads it, so expected_version guards instead of a vector.
500
records per HTTP response
79
MCP tools, in three scopes
10 MiB
of base64 content in one sync record
256
counters in one version vector
what a client must do

A client follows six rules.

Skip the second, and your client re-reads one page forever.

  1. Mint the page id locally, or call POST /v0/nodes.
  2. Carry the cursor as the seq of the last record you took.
  3. Keep a stable deviceId; bump it on every local edit.
  4. On 409 concurrent: fetch, merge, join, bump, retry.
  5. Validate against the contract package before sending.
  6. Never expect the server to resolve a sealed fork. It refuses.
the write side

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.