title → "Q3 field notes"
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.
title: "Q3 field notes"
mesh key + write access
8f3ac17e04d9…
01K…-chg_B.json
91e05c2d7a44…
status: "approved"
mesh key + write access
$ 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.
status → "approved"
8f 3a c1 7e…91 e0 5c 2d…- title
- Q3 field notes
- status
- approved
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.
$ git push --force
state.json → title changed
$ git push --force
state.json → status changed
last replacement wins
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.
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.
status → approved
mesh + HLC + op
8f 3a c1 7e 04 d9…
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.
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.
- title
- Q3 field notes
- status
- approved
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.
100 exact receipts
0 old changes
list, then skip only observed filenames
snapshot coverage
load one baseline
then pull filenames absent from its exact coverage
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.
status: active
deletedHlc: 037
blocks stale resurrection
no scalar deletion proof
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 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.
100 encrypted changes
- 1pull latest
- 2capture filenames
- 3write snapshot
- 4switch manifest
- 5remove covered changes
encrypted current rows
publish outbox → baseline → exact pull
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.
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.
Rows converge.
Local reads and writes, queued encrypted changes, field-level CRDT merge.
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.
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.Mesh authorization
Uses application authentication to grant read-only, full, or denied mesh access.
Does not give an authorized caller plaintext.
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.
Deployment details: mesh access and Worker security guardrails; file-key custody: tainted files.
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.
Generic WebDAV
- 1WebDAV credentials
- 2PROPFIND · GET · PUT · DELETE
- 3filesystem or object store
Paths and bytes are opaque; policy depends on the selected host.
ciphertext→same client protocol
Cloudflare Worker
- 1mesh address gate
- 2request authorization
- 3typed D1 / R2 operation
The payload stays opaque, while its protocol role can drive server policy.
A same-path PUT follows the host’s overwrite semantics.
Lower manifest generations and lower head.json clocks are rejected with
409.
Immutability is a client convention unless the host adds policy.
Change files, generation manifests, and mainline snapshots use insert-once storage semantics.
Access normally follows the WebDAV account or directory.
Address gates can reject unissued namespaces; middleware can grant read-only, full, or denied mesh access.
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
Related reading: Local, first, forever