Records
Every change a machine makes is one 64-byte record appended to a log file. A record names the node it belongs to, carries a version, and either holds the value inside itself or points at bytes in a data file.
The 64-byte record
| Offset | Size | Field | Holds |
|---|---|---|---|
| 0 | 16 | parent | the node ID of the parent |
| 16 | 16 | name | the encoded name under that parent |
| 32 | 8 | length | the value length in bytes |
| 40 | 8 | version | unsigned counter for this node |
| 48 | 16 | hash | the value itself, the hash of the value, or a target node ID |
Integers are little-endian. The record carries no node ID, no timestamp and no author. The ID is derived from parent and name. The author is the machine whose directory the record sits in. The position in the log gives the order the machine wrote it.
Records are a fixed size, so a log file is an array. Record number n starts at byte n × 64, and the number of records in a file is its length divided by 64.
Node IDs
Write H(x) for the first 16 bytes of SHA-256(x). A node's ID is:
id = H(parent_id ‖ name)
The root ID is the 32 hex characters that name the database's top directory. Every other ID follows from it, one step per path segment:
root = 8f3c… (from the directory name)
shared = H(root ‖ "shared")
users = H(root ‖ "users")
alice = H(users ‖ "alice")
Two machines that write the same path derive the same ID, so their records merge. A machine cannot choose an ID directly: it chooses a name under a parent, and the hash gives the ID. Producing a chosen ID under a different parent takes a second-preimage attack on SHA-256.
Truncating to 16 bytes keeps the record small. An accidental collision needs about 264 IDs in one database.
Names
A name is always 16 bytes. How those bytes are produced depends on the kind of name.
| Name | Encoding |
|---|---|
| Struct field, collection, user, area | the literal bytes, zero padded, for names of 16 bytes or fewer |
| A longer literal name | H(name) |
| Collection entry | the birth position, see below |
Literal names may not be empty and may not contain a zero byte, so "abc" and "abc\0" cannot both pad to the same 16 bytes. A name that reads as printable characters is shown as text in a path, and any other name is shown as 32 hex characters.
Entry names
An entry's name is the position its container record was first written to, plus the machine that wrote it:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | day, as a count of days since 1970-01-01 (u32) |
| 4 | 2 | log file number within that day (u16) |
| 6 | 2 | record index within that log file (u16) |
| 8 | 8 | the first 8 bytes of H(user/machine) |
Only one machine writes a given position, so entry names are unique without any random bytes. The name also answers two questions that come up constantly:
- Which machine created this entry. That machine is the one authority on whether the entry still exists.
- Which day index to consult. The container record stays in the day it was born, so that day's index settles the question.
The local server assigns the name when it writes the record, so a client cannot choose one. A client replaying a queued create gets a new position, which creates a second entry and never revives a deleted one.
Record kinds
The kind of a record follows from its fields:
| Kind | length | version | hash |
|---|---|---|---|
| Container (an entry) | 0 | 0 | zero |
| Value, 16 bytes or fewer | 0 to 16 | 1 or more | the value, zero padded |
| Value, longer | the byte count | 1 or more | H(value) |
| Reference | 0xFFFF…FF | 1 or more | the target node ID |
| Tombstone | 0 | 0xFFFF…FF | zero |
A container record is the same 64 bytes on every machine that copies it forward, so a copy merges with the original and changes nothing. A tombstone carries the maximum version, so it wins against every ordinary write for that node.
A value of 16 bytes or fewer lives in the hash field, which covers numbers, flags, short strings and every reference. Longer values live in a data file, and hash verifies them. A machine that receives a record before its data file has finished syncing shows the node as pending, and checks the hash when the bytes arrive.
A record for a node always has a non-zero parent, so a record of 64 zero bytes never occurs in ordinary data.
Merging
For each node ID, the winning record is the one with the highest version, comparing as an unsigned integer. Records with equal versions are compared by hash, byte by byte, and the higher one wins.
Two machines that hold the same set of records choose the same winner. Applying a record twice, or applying records in a different order, gives the same result.
Equal versions with different hashes also mark a concurrent edit, which the losing machine records as a conflict. Data model covers that, and Reading covers how a machine finds the winner across several machines and days.
Validating an entry
A container record is accepted when both of these hold:
- It sits in the directory of the machine named in bytes 8 to 15 of the entry name.
- That machine's index for the day named in bytes 0 to 3 lists it.
The file number and record index in the name identify the entry and are not checked, because compaction can move the record within its birth day. A record for an entry found in any other machine's directory is ignored, which stops a machine from creating entries in another machine's name.
A worked example
A small chat database. Alice's laptop creates a channel, then posts a message in it. The channel is shared, and the message belongs to alice.
shared
channels collection of Channel
<E1> name, topic
users
alice
messages collection of Message
<M1> channel (reference), text
E1 and M1 are entry names.Six records, written in this order on 2026-09-16 into log file 0:
| # | Node | length | version | hash |
|---|---|---|---|---|
| 0 | channels/E1 | 0 | 0 | zero |
| 1 | channels/E1/name | 7 | 1 | "general", inline |
| 2 | channels/E1/topic | 120 | 1 | H(topic), bytes in a data file |
| 3 | messages/M1 | 0 | 0 | zero |
| 4 | messages/M1/channel | 0xFFFF…FF | 1 | the node ID of E1 |
| 5 | messages/M1/text | 40 | 1 | H(text), bytes in a data file |
Record 0 sits at day 2026-09-16, file 0, index 0, so E1 reads:
E1 = 20712 (day) ‖ 0 (file) ‖ 0 (index) ‖ H("alice/laptop")[0:8]
The channel node ID is H(H(H(root ‖ "shared") ‖ "channels") ‖ E1), which bob's machine derives for itself the moment it reads record 0. Nothing about channels or shared is stored: the schema says those nodes exist, and the IDs follow from the path.
When bob edits the topic, he writes one record for channels/E1/topic with version 2 into his own log. When carol deletes the channel, she writes a tombstone for channels/E1. Alice's laptop, which created E1, is the machine that removes record 0 and publishes the deletion. Deleting follows that through.