Data model
The database holds one tree of nodes. Every machine keeps its own copy of the whole tree and merges the copies it receives from other machines, so two machines that have seen the same records show the same tree.
Nodes
A node either holds a value or has child nodes. A node has no name of its own inside the storage: it is identified by the name it carries under its parent, and by the node ID derived from that name. See Records for the derivation.
The application supplies a schema at runtime. The schema says which node is a struct, which is a collection, which is a value of a given type, and which is a reference. The storage stores records and merges them; the schema gives the records their meaning.
| Kind | Children | Stored as |
|---|---|---|
| Struct | a fixed set of named fields | nothing: the schema says the field exists |
| Collection | entries, each with a generated name | nothing for the collection, one container record per entry |
| Entry | the fields of the entry type | a container record written by the creating machine |
| Value | none | a value record, replaced by later versions |
| Reference | none | a value record holding the target node ID |
Struct fields and collections need no records, because their existence follows from the schema and from the path. Records exist for entries, values and references.
The top level
The root has two children, and their shape is fixed by the database:
root
shared the application's shared data
users
alice one subtree per user
bob
shared and below each user.A machine belongs to one user and writes only its own directory on disk. Where its records may land in the tree depends on the area:
| Area | Who may write records there |
|---|---|
shared/… | any machine of any user |
users/alice/… | only alice's machines |
Readers check this rule on every record and ignore a record that breaks it. Bob's machine cannot change alice's messages, and a machine that tries it only wastes space in its own log. Alice's laptop and alice's desktop both write users/alice, and their writes merge in the ordinary way.
Alice's data stays readable to everyone. The area decides who may write, and the application decides what to show.
Entries and deletion
Deletion applies to collection entries and to nothing else. A struct field keeps its node for the life of the database, so clearing a field means writing an empty value. An entry can be deleted once, and its name is never reused.
Four rules make deletion safe when machines sync in any order:
- An entry is a container. A collection of plain values stores each value one level down, at
<entry>/value. The entry's own record is written once and carries no value. - Only the creating machine writes an entry's container record. Other machines change the entry by writing its fields, which are separate nodes.
- The container record stays in the day it was written. Compaction keeps it in place, so one file on one machine always answers the question of whether the entry still exists.
- Absence hides, presence deletes. A missing record may still be in transit, so nothing is removed because of it. Records are removed when a tombstone arrives, or when a verified day index shows the creator has removed the entry.
Together these mean a deleted entry cannot come back. A machine holding a late edit under a deleted entry keeps records that no longer have a parent, and those records stay hidden until that machine cleans them up. Deleting follows the full sequence, and Compaction covers the cleanup.
Collection patterns
One collection type covers several shapes an application needs.
Set
A collection with one entry per member. Two machines adding a member at the same time create two entries, and both survive. Removing a member deletes its entry.
Optional value
A collection that holds at most one entry. Setting the value creates an entry, and replacing it deletes the old entry and creates a new one.
Tagged union
Each option is a collection, and the live option is the one holding an entry. Switching option deletes the entry in the old option and creates one in the new option. Switching back works, because the new entry gets a fresh name.
Two machines can switch to different options at the same time, which leaves an entry in each. The schema marks the union, and every machine picks the same winner: the live entry with the highest (version, name). The losing entries are deleted, and each machine that sees the same state writes the same deletion.
The same winner rule serves any constraint of the form "at most one of these may exist", which the schema can also apply across a set of references.
References
A reference is a value record whose length field is 0xFFFF…FF and whose hash field holds the target node ID. The length marks it, so a machine recognises a reference without the schema, including one that arrives long after its target was deleted.
| State of the target | What a reader shows |
|---|---|
| Live | the reference resolves |
| Unknown to this machine | the reference is hidden, and the entry holding it is hidden |
| Deleted | the reference is hidden, and the entry holding it is deleted by its own creator |
Hiding an unknown target covers the ordinary case of a reference arriving before the thing it points at. Deleting the entry that holds the reference is the cascade: delete a product, and the order lines that point at it go with it. The machine that created the order line removes its container record, which deletes the line for everyone.
The cascade stops at the nearest entry above the reference, so the schema requires an entry there. A cascading reference in a plain struct with no entry above it has nothing to delete, and the schema rejects it.
A reference that the application needs to change carries a second risk: one machine repoints it while another deletes the old target, and a third machine still holding the old value would delete the entry. The schema settles this per field:
- Cascading references are written once, when the entry is created. Pointing somewhere else means creating a new entry.
- Changeable references read as empty when their target is deleted, and never delete anything.
Chains work as expected. If A points at B and B points at C, deleting C deletes B, and that deletes A. Each step waits for the creator of the entry involved.
Versions and merging
Each node carries a version counter. A machine writing a node reads the highest version it knows and writes that version plus one. When two records exist for the same node, the higher (version, hash) wins, comparing the version as an unsigned integer and then the hash byte by byte.
The hash comparison settles ties in a way every machine agrees on, whatever order the records arrived in. A tombstone carries the maximum version, so it beats every ordinary write. Merging one record twice changes nothing, so records can travel by any route and arrive any number of times.
A machine that has yet to receive a newer version writes a version that loses. That result matches a genuine concurrent edit and settles the same way.
Conflicts
Two machines writing from the same starting version produce two records with the same version and different hashes. That pair is the signal for a conflict, and any machine holding both can see it.
The machine whose record lost writes a conflict entry into its own user area:
users/alice/conflicts/<entry>
node reference to the node that was edited
version the version both machines wrote
lostValue the value that lost
The losing machine records it because the person whose edit was dropped is the one who needs to know, and because the value is still on that machine when the loss is detected. The application decides what to do next: show a notice, offer a merge, or ignore conflicts between two machines of the same user. Resolving one means writing a new version and deleting the conflict entry.
Detection needs both records to be present at the same time, so a machine that has been away for longer than the period a superseded record is kept may miss the pair. The edit still settles correctly, with no conflict entry recorded.