botdb

docs / reference

Conflict resolution

How botdb merges concurrent changes from devices that edited offline, for rows, columns, deletes and schema.

Every device writes to its own local copy and syncs later, so two devices can change the same data without seeing each other’s changes. botdb merges them with CRDT rules: every replica that has seen the same set of changes ends up with the same data, regardless of the order they arrived in.

Changes are logical

Each local transaction becomes one batch of logical changes:

Change Meaning
insert A row exists (with its causal length).
delete A row is deleted (with its causal length).
set One cell’s new value.
schema changes Create, drop or rename tables and columns; create or drop indexes and views.

Changes are captured by triggers on each synced table, stamped with the transaction’s hybrid logical clock, and appended to the local log. Tables and columns are referenced by stable ids, and rows by their primary key encoded as a JSON array. Each batch is identified by its replica id (site) and a sequence number that increases by one per batch on that replica.

Columns: last writer wins

Each cell remembers the hybrid logical clock (HLC) timestamp and the replica id of its last write. When two devices set the same cell, the higher (timestamp, replica id) wins. Different columns of the same row merge independently.

An HLC packs 48 bits of wall-clock milliseconds and a 16-bit counter. It never goes backwards on a device, and a device that receives a newer timestamp moves its clock past it, so causally later writes always win. Ties between devices are broken by replica id.

Rows: causal length

Each row has a causal length (cl): odd means the row exists, even means it’s deleted. Inserting a row gives it cl = 1, deleting it moves it to the next even number, inserting it again to the next odd one. Replicas keep the highest cl they’ve seen.

Cell values are tagged with the row’s cl. Values written at a lower cl than the row’s current one are ignored, and last-writer-wins only compares values written at the same cl.

The result:

Examples

Device A Device B (concurrently) Result after sync
SET rating = 4 SET title = 'Dune 2' Both changes kept.
SET rating = 4 SET rating = 5, a moment later rating = 5 (later timestamp).
DELETE the row SET rating = 5 Row deleted.
DELETE the row, then re-insert SET rating = 5 before the delete Row exists with the re-inserted values.
Insert imdb_id = 'tt15239678' Insert imdb_id = 'tt15239678' One row; each column keeps the latest write.
Insert with a random id Insert the same movie, random id Two rows. Use a natural key to avoid this.

Schema

Schema changes merge the same way. Tables and columns have stable ids; their names and whether they exist are last-writer-wins registers ordered by (timestamp, replica id).

Read-only members

A member who joined with a read-only invite can’t write locally: writes fail with read_only. Relays also refuse their pushes, since they lack the write key.

Clocks

Last-writer-wins depends on wall clocks being roughly right. A device whose clock runs ahead wins concurrent writes to the same cell until real time catches up. Relays also reject requests more than 5 minutes off their clock, so keep NTP enabled.

Sync bookkeeping

Each replica tracks, per replica id, the highest contiguous sequence number it has applied. Comparing these vectors tells a replica which batches a remote is missing. Batches that arrive out of order (common with cloud folders) are kept and applied; the contiguous mark only advances once the gap is filled, so a withheld batch is detectable. Applying a batch twice has no effect.