How Interocitor syncs

How two independent operators converge without coordinating

Start with two operators who are equally privileged. Both hold the mesh key, both can read the rows, and both can write to the remote. They do not know when the other is editing, and they do not negotiate before publishing.

Interocitor makes that safe by treating each write as a separate encrypted fact. Devices exchange those facts through ordinary file storage and merge them only at trusted endpoints. The diagrams below follow that process, then show how old history is compacted without allowing deleted rows to return.

mesh://field-notes connected
operator_a
local row
title: "Q3 field notes" mesh key + write access
remote/changes
01J…-chg_A.json 8f3ac17e04d9… 01K…-chg_B.json 91e05c2d7a44…
ciphertext only
operator_b
local row
status: "approved" mesh key + write access
Both operators are trusted endpoints with full row access. They can work and publish independently; the middle stores their encrypted changes without coordinating them.
$ local.write() encrypt(change) remote.writeFile() decrypt + merge

The starting condition

Two operators. One mesh. No coordination.

Operator A and Operator B are both privileged: each holds the mesh key, can read the rows, and can write to the remote. Neither needs to know whether the other is online, acquire a lock, or wait for a leader before changing local state.

mesh://field-notestwo privileged writers
AOperator A
mesh key remote write
local edit title → "Q3 field notes"
no lock no handshake no leader
BOperator B
mesh key remote write
local edit status → "approved"
A encrypts + publishes
remote / changes
…-chg_A.json8f 3a c1 7e…
…-chg_B.json91 e0 5c 2d…
B encrypts + publishes
Either operator after pulling both changes
title
Q3 field notes
status
approved
both edits survive
Each publish creates a uniquely named encrypted change object. The operators never replace each other’s payloads; clients pull both objects and merge their field intent.

The unit of exchange is a change, not a copy of the database. Operator A publishes the title edit under a new object name; Operator B publishes the status edit under a different name. Neither upload needs to replace the other, so the remote can retain both even though it understands neither.

Once a device has both objects, it decrypts them and applies their field-level intent. The same set of changes produces the same local result regardless of which file arrived first. Coordination happens through deterministic merge after publication, rather than through a lock before publication.

One shared state.json is two force pushes.

A single shared file looks simpler, but it changes the meaning of a write. Each operator would upload an entire local view to the same path, replacing whatever was there before. Both uploads are individually valid; each is also based on a world that does not contain the other operator’s edit.

That is the storage equivalent of two people independently running git push --force against one branch. The storage provider can choose one replacement or preserve a conflict copy, but it cannot discover that the title from one file and the status from the other belong together.

operator_a $ git push --force state.json → title changed
operator_b $ git push --force state.json → status changed
shared remote path state.json last replacement wins
The provider can keep the last upload or create a conflict copy. It cannot infer that the title edit and status edit should coexist. Interocitor avoids this collision by publishing changes separately and merging them on trusted clients.

Separate change objects move the hard decision to the only place that has enough information to make it: a trusted device with the schema, merge rules, and mesh key. To see where each responsibility sits, follow one edit from the local store to the remote mailbox and back into another device.

From local edit to shared state

A change crosses the mesh.

The network is transport, not the workplace. Rows are read and written in a local store; connection work catches devices up when transport is available. This keeps a temporary network failure from becoming an application-write failure.

device_a / local

The user writes without waiting.

The row changes in the device’s local store first. The same action queues a CRDT change in the local outbox.

The user can continue reading and editing that local state while offline. When a durable local store is configured, the queued operation also survives a reload or process restart and waits for the next successful connection.

local_storeoffline
tasks / 7a
titleField notes
statusapproved
outbox 1 pending
network unavailable / local work continues
A local row update and its queued change are durable before remote sync begins.

device_a / flush

One edit becomes one opaque artifact.

On flush, Interocitor wraps the operation with its mesh identity and sortable hybrid logical clock, then encrypts the payload before calling the adapter.

The clock gives the change a stable position in the mesh’s history, while the unique object name keeps this publication distinct from every other writer’s publication. Encryption covers the row operation itself; routing information in the object path remains visible to storage.

flush --pending1 change
local operation status → approved
change envelope mesh + HLC + op
AES-GCM payload 8f 3a c1 7e 04 d9…
changes/01J7Q0…000-device_a-chg_01J7Q1….json
Unique change-entry names avoid competing writes to the payload; the sortable clock lets readers process change files deterministically.

remote / mailbox

For now, the remote is only a mailbox.

Cloudflare, Google Drive, WebDAV, or a custom adapter carries the same folder protocol. No backend needs the row schema or the merge algorithm.

At this point in the story, it only needs a growing changes/ folder. Every flush adds a uniquely named encrypted object and advances a small head marker. Other devices use that marker to decide whether there is anything new to collect.

CloudflareGoogle DriveWebDAV / NAScustom adapter
/field-notesciphertext mailbox
no plaintext · no schema · no merge
The operator can still observe transport metadata such as object names, sizes, timing, and request identity.

device_b / pull

The receiving device does the smart part.

Device B checks the remote head, downloads unseen change files, decrypts them, and applies the configured CRDT rule for each field.

Changes to different fields survive together, as in the title-and-status example. If two changes target the same field, the schema’s configured strategy decides the value. After a successful merge, the local cursor records how far this device has observed.

pull --since cursor2 unseen
device_a title “Q3 field notes”
device_b status “approved”
merged locally
title
Q3 field notes
status
approved
Different fields survive together. Same-field edits use the schema’s configured strategy.
The cursor advances after merge, so already-observed files can be skipped on the next pull.

At this point the loop is complete: local write, encrypted publication, remote transport, local merge. Repeating the loop does not make a caught-up device reread everything; its cursor lets it skip history it has already observed.

The simplicity is hiding a cost. Every successful write leaves another object in changes/. Ten edits mean ten objects; one hundred edits mean one hundred. That is still cheap for a device following along, but a new device has no cursor and no local state. It must begin at the first object and rebuild the result.

After 100 operations

Fold history into the baseline.

A caught-up device lists immutable filenames and skips only exact identities it has already observed. Compaction gives a fresh or long-absent device a bounded baseline without turning HLC order into a coverage claim.

remote/changeshead = 100
001update
002insert
···34 more
037delete row 7a
···46 more
084delete row 2c
···15 more
100update
caught-up device 100 exact receipts 0 old changes list, then skip only observed filenames
fresh device / no snapshot snapshot coverage load one baseline then pull filenames absent from its exact coverage
Compaction removes the covered files from normal listings and gives a new or rejoining device one bounded baseline to reconstruct.

After operation 100, a caught-up device still lists the change directory because a lower-HLC file may have been published late. Its exact receipt set makes the merge a no-op without confusing timestamp order for observation.

A snapshot turns the accumulated result into a new starting point. Future devices load that one encrypted baseline, restore its exact covered-filename set, and then apply every other remaining change. The snapshot must preserve the effect of deletions.

The hard part: deletion

“Deleted” must remain visible for a while.

A delete first becomes a tombstone: a small record saying this row was deleted at a particular logical time. Erasing that memory immediately would let an old offline update bring the row back.

“Row 7a is gone” remains part of the current state. The tombstone carries no old user payload, but it still wins against updates from before the deletion.

A scalar timestamp cannot prove that every device has observed every independently published file. Without a mesh-wide deletion proof, the tombstone remains retained.

01 row 7a exists status: active
37 delete arrives deletedHlc: 037
38…100 tombstone stays blocks stale resurrection
future snapshots retain tombstone no scalar deletion proof
offline device queued update @ 029

The durable queued operation is published before rehydration. CRDT order decides whether it changes the row; snapshot age never suppresses publication.

Tombstones stay in snapshots because an HLC watermark does not prove complete observation across independently publishing devices.

Tombstones remain in the baseline. A scalar watermark cannot prove that an offline device has no older queued write left to publish.

A rejoining device publishes its durable local work before replacing its cache from a newer snapshot. Every remaining immutable filename remains eligible for pull, regardless of where its HLC sorts relative to the snapshot watermark.

Compaction

The current result becomes operation zero.

A trusted device first flushes, catches up, captures the exact filenames it observed, and scans its merged rows. It writes the current state as one encrypted snapshot and points the mesh at it, then removes the exact change files captured in the snapshot.

Publication order matters. The snapshot is uploaded before the manifest points to it. A reader therefore sees either the previous complete generation or the new complete generation, never a promise of a baseline that has not been written.

before
001002···037 †···100
100 encrypted changes
  1. 1pull latest
  2. 2capture filenames
  3. 3write snapshot
  4. 4switch manifest
  5. 5remove covered changes
after
mainline/ snapshot-8-compactor.json encrypted current rows
manifest.jsonsnapshot-8
then101102
fresh or rejoining device 1 snapshot uncovered filenames publish outbox → baseline → exact pull
Snapshot first and manifest pointer last. A reader is never directed to a baseline that has not been written.

Compaction changes the cost of joining, not the meaning of the data. The snapshot is simply the same merged row state expressed as a baseline, followed by a shorter tail of changes. Protected rows remain encrypted in both forms, which brings us to the boundary between trusted devices and the storage carrying their artifacts.

The trust boundary

Keys and plaintext stay at the ends.

With a non-null key source, protected payloads are encrypted before the storage adapter receives them. This protects confidentiality, not availability.

trustedDevice A
mesh keyplaintext rows
untrustedRemote storage
ciphertext payloads names · sizes · timing remain visible
trustedDevice B
mesh keyplaintext rows
A database dump does not reveal protected row values or durable file contents, but a malicious remote can still withhold, delete, or roll back artifacts.

The remote can move and retain the mesh without being able to interpret protected payloads. It still sees operational metadata—object names, sizes, timing, manifests, and device records—and it can make the system unavailable by withholding or deleting objects. Client-side encryption provides confidentiality and per-object integrity; it does not make an untrusted storage provider available or monotonic.

This merge-and-compaction story applies to structured rows. Durable files use the same encryption boundary but a deliberately simpler lifecycle: they are addressed by path, transferred directly through the adapter, and remain until the application overwrites or deletes them.

01 / structured rows

Rows converge.

Local reads and writes, queued encrypted changes, field-level CRDT merge.

02 / durable files

Files stay exact.

Direct remote put, get, overwrite, and delete. No CRDT merge or core offline queue.

A separate decision

Encryption protects contents. Authorization protects the mesh.

These controls work together, but answer different questions. A mesh key makes a protected payload unreadable to storage; a Worker policy decides whether a request may reach that mesh at all.

client-held protection

Mesh key

Encrypts rows and ordinary durable-file bytes before they reach storage. A tainted file is sealed with an application-owned file key instead.

Taint labels identify a key scope; they do not grant remote access.
Worker request policy

Mesh authorization

Uses application authentication to grant read-only, full, or denied mesh access.

Does not give an authorized caller plaintext.
A caller normally needs both the right request access and the right key material to use protected data. Neither layer turns the Worker into a per-row or per-file ACL.

This request-authorization layer is specific to a protocol-aware backend such as the Cloudflare Worker. Generic WebDAV access follows the account or directory policy of the selected host. A named mesh such as main is a stable, predictable namespace—not a credential—and needs Worker authorization unless it is intentionally public. A checksummed address can reject an unissued namespace, but it is not proof that the caller is allowed to use one that already exists.

Application policy can be narrower than full mesh access—for example, an expiring one-time grant that permits only one opaque durable-file upload at one path. That is an application-owned Worker policy, not a built-in file ACL: a trusted client or service still records any CRDT row reference after the upload.

WebDAV and a protocol-aware backend

The backend can stay blind and still enforce guardrails.

WebDAV gives Interocitor portability by exposing ordinary file operations. The Cloudflare adapter carries the same encrypted artifacts, but its Worker can recognize protocol paths and reject some states that generic storage cannot reject portably.

With WebDAV, the server sees paths, methods, and opaque bytes. Interocitor can ask it to list a directory, read an object, write an object, or delete an object, but the portable contract cannot require the server to understand that a change file should be immutable or that a manifest generation must only move forward.

That narrow contract is what lets the same client work with a NAS, a hosted WebDAV account, or another file-oriented service. The trade-off is that authentication, overwrite behavior, quotas, and audit evidence belong to that particular host rather than to the Interocitor protocol.

same encrypted mesh / different policy surfaceadapter boundary
portable

Generic WebDAV

  1. 1WebDAV credentials
  2. 2PROPFIND · GET · PUT · DELETE
  3. 3filesystem or object store

Paths and bytes are opaque; policy depends on the selected host.

ciphertextsame client protocol
protocol-aware

Cloudflare Worker

  1. 1mesh address gate
  2. 2request authorization
  3. 3typed D1 / R2 operation

The payload stays opaque, while its protocol role can drive server policy.

Encryption and CRDT merge stay on the client in both cases. The difference is whether the storage endpoint can distinguish an arbitrary file write from a protocol operation and enforce policy around it.
Pressure Generic WebDAV contract Cloudflare Worker mitigation
Stale control write

A same-path PUT follows the host’s overwrite semantics.

Lower manifest generations and lower head.json clocks are rejected with 409.

Immutable history

Immutability is a client convention unless the host adds policy.

Change files, generation manifests, and mainline snapshots use insert-once storage semantics.

Who may use a mesh

Access normally follows the WebDAV account or directory.

Address gates can reject unissued namespaces; middleware can grant read-only, full, or denied mesh access.

Abuse and operations

Limits, logs, and retention are provider-specific.

Typed body limits, a per-mesh durable-file quota, upload policy, structured audit events and TTL maintenance are available.

These controls mitigate stale clients, accidental overwrites, unauthorized requests, and unbounded uploads that pass through the normal Worker API. They do not make the remote trustworthy: a compromised host can still withhold or delete objects, restore an older database, or bypass the Worker. The backend still sees metadata, and its authorization boundary is the mesh—not an individual row, document, or recipient.

The generation and clock checks are monotonic floors, not compare-and-swap locks. They reject a move backward through the normal API, but they do not serialize equal-generation compaction races.

A particular WebDAV server may offer comparable controls through custom modules, storage rules, or an application gateway. Interocitor cannot depend on those features through the generic WebDAV interface. Once the server recognizes Interocitor path types and validates their meaning, it is effectively another protocol-aware backend.

The whole design is one repeatable loop.

Operators change local state without waiting for one another. Each publication is an encrypted, uniquely named change; ordinary storage carries it; trusted devices merge it. Cursors make routine pulls incremental, and snapshots keep a new device’s catch-up work bounded as history grows.

The reason this works is the separation of responsibilities. Storage preserves artifacts but does not resolve intent. Devices hold keys, interpret schemas, merge concurrent edits, and decide when history is safe to compact. Interocitor does not distinguish a row migration from another application write: the application computes the update; Core transports and merges it.

  • Change locallyinstant app state
  • Queue + encryptopaque artifact
  • Store anywherecommodity mailbox
  • Pull + mergetrusted device
  • Compact laterfresh baseline