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.

KindChildrenStored as
Structa fixed set of named fieldsnothing: the schema says the field exists
Collectionentries, each with a generated namenothing for the collection, one container record per entry
Entrythe fields of the entry typea container record written by the creating machine
Valuenonea value record, replaced by later versions
Referencenonea 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
The fixed top level. The application's schema supplies the shape below 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:

AreaWho 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:

  1. 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.
  2. Only the creating machine writes an entry's container record. Other machines change the entry by writing its fields, which are separate nodes.
  3. 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.
  4. 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 targetWhat a reader shows
Livethe reference resolves
Unknown to this machinethe reference is hidden, and the entry holding it is hidden
Deletedthe 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:

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
A conflict entry. The reference makes the conflict disappear when the edited entry is deleted.

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.

Read Records for the byte layout of everything described here, and Writing for the steps a machine takes when the application changes a value.

Related