Architecture tour
One edit, followed end to end: you rename a task on your laptop and your phone shows the new title. Each step names the file to open. The reference is Architecture; the protocol is Sync protocol.
On this page
On the laptop#
- The click. The title field in the task panel (
web/src/app/) calls the data layer; screens never write to the database themselves. mutate()inweb/src/lib/ops.tsturns the change into one field-level operation,{entity: "task", entityId, field: "title", value, ts, device}, with a hybrid logical clock timestamp (web/src/lib/hlc.ts). In one IndexedDB transaction (Dexie,web/src/lib/db.ts) it updates the local row, so the list re-renders at once, and appends the op to theoutbox. Several changes at once go throughmutateMany().- The exchange.
web/src/lib/sync.tssends the outbox and the device's cursor in onePOST /api/v1/sync. Offline, the op waits in the outbox; nothing is lost.
On the server#
- The route.
internal/httpapichecks the session or token through the route's policy (internal/httpapi/policy.go), then hands the ops tointernal/sync. - The op log.
sync.Service.Apply(internal/sync/service.go) validates each op against the schema (internal/sync/ops.go), resolves its sharing scope, checks the author's role, appends it to theopstable and keeps the winning value per field inscope_fields: last writer wins byts, thendevice. Applying the same ops twice, or in another order, gives the same state. - The answer. The response carries the ops this device has not seen yet and its new cursor. A refused op is reported per op and never blocks the outbox.
- The wake-up. After the commit,
NotifyAppliedtells every open event stream (GET /api/v1/sync/events) of everyone who can see the task that the cursor moved. The stream carries no data.
On the phone#
- The pull. The phone's stream wakes
sync.ts, which runs the same exchange: its outbox (empty) up, the new op down. - The merge. The client applies the op with the same last-writer-wins rule as the server, updates the row in IndexedDB, and the open list re-renders.
What else writes#
Every other writer takes the same path from step 5: the REST API and the ot command (ApplyStrict, all or nothing), CalDAV, imports, automation rules, email to task, calendar sync and AI agents. The table of writers and their device ids is in Architecture. Accounts, tokens, settings and membership are not ops: they are ordinary tables behind authenticated routes.
Where to read next#
- A new synced field: declare it once in
internal/sync/ops.goSchemaand regenerate (seeAGENTS.md, rule 3). - Logic that must match on both sides: Add a shared contract.
- Background work: one entry in
internal/tenant/jobs.go(Background work).