This is the full developer documentation for lattice # lattice > Conflict-free replicated data types (CRDTs) for Gleam, with property-tested merge semantics. Convergence notebook ## Local updates, deterministic merges, one shared value. lattice gives each replica a CRDT state it can update independently. When replicas meet, merge uses the CRDT's lattice rule so every node converges without locks, consensus, or a conflict-resolution callback. GCounter merge ### Merge components first. Sum only when reading. * node-a local state `{a: 2, b: 0}` * node-b local state `{a: 0, b: 3}` * Component-wise merged state `{a: max(2, 0), b: max(0, 3)} = {a: 2, b: 3}` Derived read value`sum(values) = 2 + 3` **5** Merge compares each replica component independently. The user-facing read sums the components only after that merge. [Trace the GCounter rule](/guides/counters/)[Why merge order does not change the result](/introduction/#what-is-a-crdt) Evaluating lattice? ### Take the route that answers your next question. [Understand the model**See what CRDT convergence means and where each data type fits.**](/introduction/)[Inspect package boundaries**Compare the umbrella package with focused dependencies.**](/packages/) ## Choose the shape of your state. Start from the data you need to replicate, then install the smallest package that carries that merge rule. [Total**Counters***`lattice_counters` for values that only grow, or grow and shrink.*](/guides/counters/)[Single value**Registers***`lattice_registers` for last-writer-wins or multi-value conflicts.*](/guides/registers/)[Membership**Sets + presence***`lattice_sets` and `lattice_presence` for collections and live membership.*](/guides/sets/)[Nested state**Maps + text***`lattice_maps`, `lattice_sequence`, and `lattice_text` for keyed or ordered data.*](/guides/maps/) Release status and versioning Published lattice packages use Semantic Versioning (SemVer); current Hex releases include 1.x packages and the 3.x `lattice_crdt` umbrella. Pin the exact versions you validate and review release notes before upgrading, especially across major versions. `lattice_fugue` and `lattice_text_fugue` are not published to Hex yet and may change before their first release. ## Prototype safely. Verify the merge behavior your application depends on, then pin the package versions you validated. Recheck behavior and release notes before upgrading. [Quick Start**Build and merge your first counter.**](/quick-start/)[Installation**Choose a package, then pin the version you validate.**](/installation/)[Correctness model**Review why independent updates converge.**](/introduction/#what-is-a-crdt) # Delta-State Replication > Sending compact CRDT deltas instead of full state. State-based CRDTs are simple to reason about: every replica can merge another replica's state and converge. Delta-state replication keeps that merge model, but sends only the change produced by a local mutation. Use delta-state APIs when replicas sync frequently over transports such as websockets, gossip, or reconnect catch-up, where sending the full CRDT after every small update would waste bandwidth. ## Leaf CRDT convention [Section titled “Leaf CRDT convention”](#leaf-crdt-convention) Each state-changing operation has a `*_with_delta` companion. Fallible operations return `Result`: ```gleam let assert Ok(#(local, delta)) = g_counter.increment_with_delta(local, 5) let remote = g_counter.merge(remote, delta) ``` The first tuple item is the new local state. The second item is itself a CRDT of the same type containing the change. Remote replicas merge that delta with the same `merge` function used for full-state replication. State-only operations discard the successful delta and preserve errors. Counter, sequence, and text edits return `Result` under plain names, without panicking variants. Infallible set operations still return their state directly. ## Example: counter deltas [Section titled “Example: counter deltas”](#example-counter-deltas) ```gleam import lattice_core/replica_id import lattice_counters/g_counter pub fn main() { let local = g_counter.new(replica_id.new("node-a")) let remote = g_counter.new(replica_id.new("node-b")) let assert Ok(#(local, delta)) = g_counter.increment_with_delta(local, 5) let remote = g_counter.merge(remote, delta) g_counter.value(remote) // -> 5 } ``` The delta carries only the changed replica entry, not every replica count in the counter. ## Sequence and text identity [Section titled “Sequence and text identity”](#sequence-and-text-identity) Both sequence backends and their text wrappers require the receiving editor's identity when merging a state or delta: ```gleam let assert Ok(#(local, delta)) = sequence.insert_with_delta(local, 0, value) let remote = sequence.merge(remote, delta, remote_id) ``` `merge(delta, remote, remote_id)` has the same result. `merge_as` is an alias with the same three arguments. Use a fixed output identity when applying merge laws, and keep each independent writer's identity unique. ## ORMap deltas [Section titled “ORMap deltas”](#ormap-deltas) `ORMap(a)` tracks generation-qualified membership and stores typed CRDT children. It uses an opaque `ORMapDelta(a)` instead of representing a map delta as a full snapshot. ```gleam import lattice_core/replica_id import lattice_counters/g_counter import lattice_maps/crdt import lattice_maps/or_map fn add_points(value: crdt.Crdt(String)) -> crdt.Crdt(String) { case value { crdt.CrdtGCounter(counter) -> { let assert Ok(counter) = g_counter.increment(counter, 5) crdt.CrdtGCounter(counter) } other -> other } } pub fn main() { let local = or_map.new(replica_id.new("node-a"), crdt.GCounterSpec) let remote = or_map.new(replica_id.new("node-b"), crdt.GCounterSpec) let assert Ok(#(local, delta)) = or_map.update_with_delta(local, "alice", add_points) let assert Ok(remote) = or_map.apply_delta(remote, delta) or_map.keys(remote) // -> ["alice"] } ``` The callback above returns the full child counter. `update_with_delta` avoids sending unrelated keys, but does not make that child payload sparse. For a large Sequence or Text, use the sparse callback API and return the delta from its `*_with_delta` operation. `CrdtDelta(a)` distinguishes: | Variant | Payload | | -------------------- | ------------------------------------------------ | | `NoChange(spec)` | No leaf change for the given schema. | | `StateDelta(child)` | A leaf delta or an explicit full child snapshot. | | `OrMapChange(delta)` | A sparse nested ORMap delta. | The map applies the callback's delta to produce local state and emits the same change to peers. Returning a nested `OrMapChange` preserves sparse updates through ORMap paths. Do not return the child operation's full updated state when you need a sparse leaf payload. For a newly created key, the map also transmits required initial state. A configured register seed is not an empty merge value, even when the callback returns `NoChange`. Removal-only entries also carry their generation. A newer generation supersedes older membership and values, even when the newer key is absent. Same-generation update/remove conflicts remain add-wins. ## Batching deltas [Section titled “Batching deltas”](#batching-deltas) Transport layers can combine pending ORMap deltas before sending them: ```gleam let assert Ok(combined) = or_map.merge_deltas(delta_a, delta_b) let assert Ok(remote) = or_map.apply_delta(remote, combined) ``` Store unacknowledged deltas in a per-peer outbox, join them with `merge_deltas`, and send the result. Batching sparse nested ORMap changes stays sparse. A batch containing a full snapshot can remain a snapshot. Remove acknowledged outbox data only when the transport's recovery contract permits it. ## Serialization [Section titled “Serialization”](#serialization) Leaf deltas use their state codecs. Typed register/set payloads use the generic codec entry points with the application's encoder and decoder. `ORMapDelta(a)` has separate helpers for recursive delta payloads: ```gleam let encoded = or_map.delta_to_json(delta) let decoded = or_map.delta_from_json(json_string) ``` Use those helpers when sending ORMap deltas across a process, node, or browser boundary. ## Transport responsibilities [Section titled “Transport responsibilities”](#transport-responsibilities) lattice provides CRDT values and merge semantics. A websocket or gossip layer is still responsible for peer identity, buffering, acknowledgements, reconnect policy, and deciding when to fall back to full-state sync. Transports can tolerate duplicate and reordered messages, but must supply a baseline or the required earlier deltas. A later Sequence edit can reference items not yet received; an intermediate view can be incomplete. One latest delta does not reconstruct an arbitrary empty peer. Outer-map acknowledgments and pruning clocks do not establish a child Sequence/Text compaction frontier. Keep those stability decisions separate. LWWMap assignments carry complete snapshots and choose one winner; they are outside the sparse ORMap path guarantee. # JSON Serialization > Serializing and deserializing CRDTs with JSON. Every CRDT module across all lattice packages exposes `to_json` and `from_json`. Most encoded CRDTs include a `"type"` discriminator and schema version. Presence retains its unversioned replication format, described below. ## Basic example [Section titled “Basic example”](#basic-example) ```gleam import gleam/json import lattice_core/replica_id import lattice_counters/g_counter pub fn main() { let assert Ok(counter) = g_counter.new(replica_id.new("node-a")) |> g_counter.increment(3) let encoded = counter |> g_counter.to_json |> json.to_string case g_counter.from_json(encoded) { Ok(decoded) -> g_counter.value(decoded) Error(_) -> 0 } // -> 3 } ``` ## ORMap serialization [Section titled “ORMap serialization”](#ormap-serialization) `ORMap` also supports JSON round-tripping: ```gleam import gleam/json import lattice_core/replica_id import lattice_counters/g_counter import lattice_maps/crdt import lattice_maps/or_map fn add_points(value: crdt.Crdt(String)) -> crdt.Crdt(String) { case value { crdt.CrdtGCounter(counter) -> { let assert Ok(counter) = g_counter.increment(counter, 1) crdt.CrdtGCounter(counter) } other -> other } } pub fn main() { let assert Ok(map) = or_map.new(replica_id.new("node-a"), crdt.GCounterSpec) |> or_map.update("alice", add_points) let encoded = map |> or_map.to_json |> json.to_string case or_map.from_json(encoded) { Ok(decoded) -> or_map.keys(decoded) Error(_) -> [] } // -> ["alice"] } ``` Modern map snapshots include recursive child schemas and the metadata needed to preserve generations or LWW assignment order. Typed leaves retain their causal state rather than encoding only visible values. Use caller-supplied payload encoders and decoders for `Crdt(a)` values such as integers or records. Existing String leaf codec entry points retain their formats. A register's configured initial value belongs to the schema so newly created keys after load use the same default. Dispatch distinguishes Text from `Sequence(String)` with a Text wrapper. The standalone Text codec still uses its canonical Sequence envelope; a bare Sequence envelope decodes as Sequence. ## ORMap delta serialization [Section titled “ORMap delta serialization”](#ormap-delta-serialization) `ORMapDelta(a)` has dedicated JSON helpers because it is not a full map: ```gleam import gleam/json import lattice_maps/or_map let encoded = delta |> or_map.delta_to_json |> json.to_string let decoded = or_map.delta_from_json(encoded) ``` Use the delta helpers for map-level messages. Nested `OrMapChange` payloads remain deltas rather than full child snapshots. Preserve generation floors even for removed keys, plus leaf counters, item IDs, move/delete records, frontiers, and forwarding metadata. ## Legacy map import [Section titled “Legacy map import”](#legacy-map-import) Modern map formats require a coordinated migration. Import an agreed legacy baseline, distribute the modern snapshot, then switch writers. Do not mix legacy map deltas with generation-aware replication. ORMap snapshots use version 3 and deltas use version 2; LWWMap snapshots use version 3. Legacy ORMap entries enter the initial generation. Supply an explicit payload schema/default when the old String spec cannot determine it. An importer cannot recover history that an old writer already pruned; use a fresh writer identity where allocation history is unavailable. Legacy String LWWMap imports wrap scalar values as register children and retain the original String tie keys. Modern entries use writer identity. At equal timestamp and tombstone status, modern entries outrank legacy entries; legacy-versus-legacy comparisons keep the old rule. The import APIs require an explicit schema and receiving identity: ```gleam import gleam/dynamic/decode import lattice_core/replica_id import lattice_maps/crdt import lattice_maps/or_map pub fn import_string_register_map(legacy_snapshot: String) { or_map.import_legacy( legacy_snapshot, crdt.LwwRegisterSpec(""), decode.string, replica_id.new("new-writer"), ) } ``` For a scalar String LWWMap baseline, call `lww_map.import_legacy(snapshot, crdt.LwwRegisterSpec(""), local_id)`. Its modern children are LWWRegisters rather than raw strings. LWWMap snapshots must contain one entry at most for each exact key. Modern decoding and legacy v1/v2 import reject repeated live entries, tombstones, and live/tombstone pairs rather than selecting an array-order winner. Producers of previously accepted duplicate entries must resolve each key before encoding or before calling `import_legacy`. Key identity is exact and is not Unicode-normalized. Decoding rejects unsupported versions, incompatible schemas, unsafe allocation metadata, and conflicting immutable write identities. Use the receiving editor's identity when adopting decoded state for edits. ## Presence serialization [Section titled “Presence serialization”](#presence-serialization) `lattice_presence/presence_state` serializes distributed presence state for cross-node replication: ```gleam import lattice_presence/presence_state let payload = presence_state.to_json_string(state) let decoded = presence_state.from_json(payload) ``` Presence JSON contains only replicated CRDT data: replica name, causal context, clouds, and presence entries. Local replica visibility state from `replica_up`/`replica_down` is intentionally not encoded. Decoding validates clock values and limits nested metadata depth before returning `Ok(state)`. Use `presence_state.decoder()` to embed presence state in a larger JSON decoder. The former `state_json` module and public replicated-parts constructor are removed; the wire representation is unchanged. ## Sequence snapshots and local identity [Section titled “Sequence snapshots and local identity”](#sequence-snapshots-and-local-identity) Sequence and text decoding restores the serialized replica identity. Before editing a snapshot received from another replica, merge it with your local state using `merge(local, decoded, local_id)`. Do not edit remote state under the sender's identity. YATA sequence and text state use schema v2. V1 states without moves decode directly; v1 states with moves and no compacted blocks reconstruct base order. V1 states with both moves and compacted blocks are rejected and require a resync. Fugue retains schema v1. # Version Vectors > Understanding version vectors and causal context for OR-types. Version vectors are the causal backbone of lattice's observed-remove CRDTs. They are provided by the `lattice_core` package. ## What is a version vector? [Section titled “What is a version vector?”](#what-is-a-version-vector) A version vector is a map from replica IDs to logical clock values. It tracks which events each replica has seen, enabling detection of causal ordering between states. ## Creating and incrementing [Section titled “Creating and incrementing”](#creating-and-incrementing) ```gleam import lattice_core/replica_id import lattice_core/version_vector pub fn main() { let node_a = replica_id.new("node-a") let node_b = replica_id.new("node-b") let vv = version_vector.new() |> version_vector.increment(node_a) |> version_vector.increment(node_a) |> version_vector.increment(node_b) version_vector.get(vv, node_a) // -> 2 version_vector.get(vv, node_b) // -> 1 } ``` ## Comparing version vectors [Section titled “Comparing version vectors”](#comparing-version-vectors) `version_vector.compare` returns one of four `Order` variants: * `Equal` — both vectors have the same clock values * `Before` — `a` happened before `b` (every clock in `a` is less than or equal to the corresponding clock in `b`, and at least one is strictly less) * `After` — `a` happened after `b` * `Concurrent` — neither vector dominates the other ```gleam import lattice_core/replica_id import lattice_core/version_vector pub fn main() { let node_a = replica_id.new("node-a") let node_b = replica_id.new("node-b") let vv_a = version_vector.new() |> version_vector.increment(node_a) |> version_vector.increment(node_a) let vv_b = version_vector.new() |> version_vector.increment(node_b) version_vector.compare(vv_a, vv_b) // -> Concurrent (vv_a has higher node-a, vv_b has higher node-b) } ``` ## Merging [Section titled “Merging”](#merging) Merge takes the pairwise maximum of every clock value: ```gleam let merged = version_vector.merge(vv_a, vv_b) version_vector.get(merged, node_a) // -> 2 version_vector.get(merged, node_b) // -> 1 ``` ## Dominates [Section titled “Dominates”](#dominates) `version_vector.dominates(a, b)` returns `True` when `a` has seen everything `b` has seen — every clock in `a` is greater than or equal to the corresponding clock in `b`. ## Where version vectors are used [Section titled “Where version vectors are used”](#where-version-vectors-are-used) You rarely need to work with version vectors directly. They are used internally by: * `MVRegister` — to track causal history of writes and detect concurrent updates * `ORSet` — via pruned version vectors for tombstone garbage collection * `ORMap` — prunes membership tags in each key/generation scope ORMap membership IDs include their key and generation namespace. A stability vector must acknowledge those membership tags, not an unrelated logical-writer counter. It does not authorize compaction of a child Sequence/Text or deletion of generation floors. ## DotContext [Section titled “DotContext”](#dotcontext) A `DotContext` tracks individual observed events — "dots" consisting of a replica ID and a counter value. It is used internally by causal CRDTs to determine which add operations have been observed by a given replica. You typically do not interact with `DotContext` directly unless you are building custom CRDTs on top of lattice's causal infrastructure. # Counters > Using GCounter and PNCounter for distributed counting. Counters are provided by the `lattice_counters` package. If you installed the `lattice_crdt` umbrella, they are already available. Use counters when replicas need to accumulate numeric state and later merge without coordination. ## GCounter (Grow-only Counter) [Section titled “GCounter (Grow-only Counter)”](#gcounter-grow-only-counter) `GCounter` is grow-only. Each replica can increase its own contribution, and merge takes the per-replica maximum before summing the result. ```gleam import lattice_core/replica_id import lattice_counters/g_counter pub fn main() { let assert Ok(left) = g_counter.new(replica_id.new("node-a")) |> g_counter.increment(2) let assert Ok(right) = g_counter.new(replica_id.new("node-b")) |> g_counter.increment(5) let merged = g_counter.merge(left, right) g_counter.value(merged) // -> 7 } ``` `g_counter.increment` returns `Result`. A negative delta returns `Error(NegativeDelta(delta))` without changing the counter. Handle this error when amounts come from external input; the examples assert known positive values. ## PNCounter (Positive-Negative Counter) [Section titled “PNCounter (Positive-Negative Counter)”](#pncounter-positive-negative-counter) `PNCounter` supports both increments and decrements by pairing two internal `GCounter`s. ```gleam import lattice_core/replica_id import lattice_counters/pn_counter pub fn main() { let assert Ok(counter) = pn_counter.new(replica_id.new("node-a")) |> pn_counter.increment(10) let assert Ok(counter) = pn_counter.decrement(counter, 3) pn_counter.value(counter) // -> 7 } ``` Both `pn_counter.increment` and `pn_counter.decrement` require non-negative deltas. To subtract 3, call `pn_counter.decrement(counter, 3)` rather than passing `-3`. Both operations return `Result`, including their delta variants. ## Delta-state mutators [Section titled “Delta-state mutators”](#delta-state-mutators) Counters also expose delta-aware mutators: * `g_counter.increment_with_delta` * `pn_counter.increment_with_delta` * `pn_counter.decrement_with_delta` Each returns `Ok(#(new_state, delta))` or a typed error. Remote replicas apply the delta with the existing `merge` function. See [Delta-State Replication](/advanced/delta-state/) for the shared convention. The former `try_increment`, `try_decrement`, and corresponding delta names have been replaced by the plain Result-returning names. # Maps > Typed recursive maps with observed-remove or last-writer-wins semantics. The `lattice_maps` package provides `ORMap(a)` and `LWWMap(a)`. Both store String keys and `Crdt(a)` children, including nested maps. The `lattice_crdt` umbrella includes them. Choose the container by its merge behavior: | Container | Concurrent child changes | | --------- | -------------------------------------------- | | ORMap | Join child CRDTs within the same generation. | | LWWMap | Select one complete child assignment. | ## Typed child specifications [Section titled “Typed child specifications”](#typed-child-specifications) Each map has one `CrdtSpec(a)` that defines its children and their initial state. Parameterized registers, sets, and sequences share payload type `a`. Text has a concrete String/grapheme payload. For mixed application data, use a tagged union as `a` and supply its JSON encoder and decoder. ```gleam import lattice_core/replica_id import lattice_maps/crdt import lattice_maps/or_map let local = replica_id.new("node-a") let documents: or_map.ORMap(String) = or_map.new(local, crdt.TextSpec) let lists: or_map.ORMap(Int) = or_map.new(local, crdt.SequenceSpec) let boards: or_map.ORMap(String) = or_map.new(local, crdt.OrMapSpec(crdt.LwwRegisterSpec(""))) ``` `LwwRegisterSpec(initial_value)` supplies an actual initial payload. Nested `OrMapSpec(child_spec)` and `LwwMapSpec(child_spec)` create empty maps with the chosen schema. A changed register value need not equal its configured initial value. Map updates and merges return errors for incompatible child kinds or recursive schemas. A rejected update does not activate a missing or removed key. ## ORMap updates [Section titled “ORMap updates”](#ormap-updates) The full-value `update` and `update_with_delta` callbacks receive the current child. A first update creates the configured default. Within a generation, the map joins the returned child through the same apply path used for replication. Use the sparse callback API for large Sequence/Text values. Perform the leaf's `*_with_delta` operation and return `StateDelta` containing its delta. For a nested ORMap, return `OrMapChange` containing the child map's delta. The map computes local state from that change, so local updates and remote application use the same payload. A leaf no-op can still refresh key membership. Callback failures and schema errors do not emit a successful map delta. ### Sparse Text example [Section titled “Sparse Text example”](#sparse-text-example) ```gleam import gleam/result import lattice_core/replica_id import lattice_maps/crdt import lattice_maps/or_map import lattice_text/text pub fn main() { let documents: or_map.ORMap(String) = or_map.new(replica_id.new("node-a"), crdt.TextSpec) or_map.update_delta(documents, "notes", fn(value, _context) { let assert crdt.CrdtText(document) = value text.insert_with_delta(document, 0, "hello") |> result.map(fn(pair) { let #(_updated, delta) = pair crdt.StateDelta(crdt.CrdtText(delta)) }) }) } ``` The result contains the updated map and its transport delta. For ordered lists, select `SequenceSpec` and wrap the Sequence operation's delta in `CrdtSequence`. The runnable [Sequence/Text example](https://github.com/tylerbutler/lattice/blob/main/examples/src/or_map_sequence_text_example.gleam) also covers typed snapshot adoption and moves. ### Removal and re-addition [Section titled “Removal and re-addition”](#removal-and-re-addition) Within one generation, removal retracts observed membership tags. An unobserved concurrent update survives under add-wins semantics. Re-adding a removed key starts a fresh empty/default generation. A newer generation replaces the old generation's membership and child state. This also replaces old-generation edits concurrent with the reset. Concurrent re-adds choose one winner by generation clock, then replica ID in lexicographic UTF-8 byte order on both runtimes. The map retains a generation floor even after removal. A delayed older snapshot cannot reactivate an older value when the newer generation is absent. It must not supply fallback content for a newer generation. ### Identity and pruning [Section titled “Identity and pruning”](#identity-and-pruning) Map operations bind child editing identities to the local writer, enclosing path, and generation. They preserve historical item IDs and write authors. Adopt received snapshots under your local identity before editing, including keys that existed only on the sender. Use `or_map.bind(map, local_id)` or `or_map.merge_as(local, received, local_id)`. The two-argument `merge` retains the left map's identity. The current generation's inactive leaf history remains until a newer generation supersedes it. Pruning can discard superseded payloads but must retain generation and allocation floors. Outer key clocks do not authorize Sequence/Text compaction or forwarding expiry. ## LWWMap assignments [Section titled “LWWMap assignments”](#lwwmap-assignments) LWWMap has the same recursive child schema model, but each assignment stores one complete child snapshot. A map can contain Text, Sequence, or another map without changing this atomic replacement rule. Sets and removals must not precede the key's timestamp and must be strictly above the pruning threshold. Local writes and merge use the same order: 1. Greater timestamp. 2. Tombstone over active value at equal timestamps. 3. Greater writer identity in lexicographic UTF-8 byte order for modern writes. Generic child payloads are never compared to break a tie. Different active payloads claiming the same modern timestamp and writer are invalid and return `ConflictingWrite`; an active/tombstone conflict at that stamp selects the tombstone. A timestamp at or below the prune floor remains rejected, including for a key whose tombstone was pruned. Use increasing timestamps to express successive writes. Equal timestamps represent competing assignments: a local set can lose to the stored writer, and an equal-time remove wins. Before writing received state, use `bind` or `merge_as` to set the intended local writer identity. Child snapshots remain immutable under their outer write identity; preparing a new child edit uses a fresh assignment editing scope. Two concurrent edits to a Text assigned through LWWMap do not both survive. Use an ORMap-only path to that Text when collaborative child merge and sparse leaf updates are required. ## Delta delivery [Section titled “Delta delivery”](#delta-delivery) `ORMapDelta(a)` carries touched keys, generations, membership changes, and child deltas. `CrdtDelta(a)` separates leaf/full-state payloads from nested ORMap changes. Batching sparse ORMap changes preserves their sparse form; an explicit full snapshot can produce a full-state batch. Receivers need a baseline or eventual delivery of the required history. A later Sequence edit alone cannot reconstruct prior items. Duplicate and reordered delivery converges after required origins arrive, but an intermediate view can be incomplete. LWWMap uses full-state payloads. Its atomic boundary is outside the sparse ORMap leaf-size guarantee. ## Migrating legacy maps [Section titled “Migrating legacy maps”](#migrating-legacy-maps) The modern map formats include recursive schemas and generation or write metadata. Import an agreed legacy baseline and distribute the modern snapshot before switching writers. Do not send legacy map deltas into modern generation-aware replication. Legacy ORMap entries start in the initial generation. Supply an explicit schema/default where an old String spec cannot determine the new payload type. Previously pruned allocation history cannot be recovered; use a fresh writer identity if that history is unavailable. Legacy LWWMap String imports wrap values as register children and retain the old String tie keys. Legacy entries compare these keys in lexicographic UTF-8 byte order; modern writes use writer identity and outrank legacy entries at equal timestamp and tombstone status. ### Unicode ordering and upgrades [Section titled “Unicode ordering and upgrades”](#unicode-ordering-and-upgrades) Legacy value ties and modern writer ties use the same order on Erlang and JavaScript, without Unicode normalization. For example, an imported legacy value `"\u{10000}"` wins over `"\u{e000}"` at an equal timestamp in either merge order. For modern assignments, that example applies to writer IDs, not to the child payloads. Older JavaScript versions used UTF-16 order and chose `"\u{e000}"` for the legacy value conflict. Erlang's ordering is unchanged. Upgrade peers that exchange affected Unicode values or IDs together; peers using different ordering rules can disagree. Coordinate this with the legacy-map cutover above, rather than mixing legacy deltas with modern generations. An upgrade cannot restore a losing value that no replica retained. The [Replica IDs guide](/guides/replica-ids/#unicode-ordering-and-upgrades) describes the related correction for replica-ID ties and historical sequence state. See [Delta-State Replication](/advanced/delta-state/), [JSON Serialization](/advanced/serialization/), and [Replica IDs](/guides/replica-ids/). # Presence > Tracking distributed presence with add-wins CRDT semantics. Presence is provided by the `lattice_presence` package. It tracks which pids are present for a topic and key, together with arbitrary JSON metadata. Use it when multiple nodes need to maintain a shared view of online users, socket processes, sessions, or similar ephemeral membership without routing all joins and leaves through one coordinator. ## Basic joins and reads [Section titled “Basic joins and reads”](#basic-joins-and-reads) ```gleam import gleam/json import lattice_presence/presence_state as presence pub fn main() { let state = presence.new_incarnation("node-a") |> presence.join("pid-1", "room:lobby", "alice", json.object([])) presence.get_by_topic(state, "room:lobby") // -> [#("pid-1", "alice", _)] } ``` Each `join` creates a causal tag owned by the local replica. Queries hide entries from replicas you have marked down locally. ## Process restarts [Section titled “Process restarts”](#process-restarts) Replica identity uniqueness is per process incarnation, not just per stable node name. Create a fresh identity every time the presence process starts: ```gleam let state = presence.new_incarnation("node-a") ``` This preserves `node-a` as the stable name while giving each run a unique causal identity. On merge, a restarted state ignores cached values owned by an earlier incarnation of that stable name but retains their causal context. Syncing the restarted state back to peers therefore removes those stale entries. Use `new` only when the supplied identity is already unique for the entire process incarnation and will never be reused after restart. ## Merging replicas [Section titled “Merging replicas”](#merging-replicas) `merge` returns the merged state on success: ```gleam let assert Ok(merged) = presence.merge(node_a, node_b) ``` Use `merge_with_diff` when an application needs Phoenix-style join and leave notifications while applying remote state: ```gleam let assert Ok(#(merged, diff)) = presence.merge_with_diff(node_a, node_b) ``` The diff groups joins and leaves by topic. It is for notifying subscribers; the merged state is still the source of truth. Both merge functions return `Error(SameReplica(...))` when divergent states use the same replica identity, or when a peer carries local-owned tags or causal history that the local state has not observed. This catches restart echoes relayed through another replica, even after the old entries have been removed. Gossip of already-known local tags remains valid, and identical same-replica states are an idempotent no-op. Discard stale restart echoes or assign each live replica a unique identity before retrying; the check does not replace the requirement for unique incarnation identities. ## Replica visibility [Section titled “Replica visibility”](#replica-visibility) Replica liveness is local view state, not replicated CRDT state. If a node sees a peer go down, it marks that peer down in its own state: ```gleam let #(state, diff) = presence.replica_down(state, "node-b") ``` Entries owned by a down replica become invisible to query functions and appear as leaves in the returned diff. If the replica comes back: ```gleam let #(state, diff) = presence.replica_up(state, "node-b") ``` Those entries become visible again and appear as joins. This keeps cluster liveness decisions in the embedding application instead of trying to replicate up/down status as CRDT data. ## Leaving and cleanup [Section titled “Leaving and cleanup”](#leaving-and-cleanup) `leave` removes one local pid/topic/key entry: ```gleam let state = presence.leave(state, "pid-1", "room:lobby", "alice") ``` `leave_by_pid` removes all local entries for a pid. Both operations only remove entries owned by the local replica; foreign entries must be removed by their owning replica or hidden with replica liveness. After your application decides that a replica will not return with useful state, mark it down and prune its entries: ```gleam let #(state, diff) = presence.replica_down(state, "node-b") let state = presence.remove_down_replica(state, "node-b") ``` Use `diff.leaves` to notify subscribers. `remove_down_replica` requires Down status; otherwise it returns the state unchanged. It removes entries, sparse clouds, and local liveness, but retains the maximum observed context/cloud clock as a causal high-water mark. Stale gossip cannot restore tags covered by that mark. ### Superseding a peer incarnation [Section titled “Superseding a peer incarnation”](#superseding-a-peer-incarnation) Use `supersede` after your membership or restart protocol establishes which incarnation of a peer is current. Pass that full identity, not just its base name. The helper downs and prunes other known identities with the same base, including those already marked Down: ```gleam case presence.supersede(state, current_replica) { Ok(#(state, diff)) -> Ok(#(state, diff.leaves)) Error(error) -> Error(error) } ``` On success, keep the returned state and use the combined leaves for subscriber notifications. The diff groups `#(key, pid, meta)` entries by topic. Joins are empty, and entries already hidden by `replica_down` do not produce another leave. A repeated call with no new intervening entries returns the same state and an empty diff. The selected incarnation need not be present. The helper leaves its data and liveness unchanged, so call `replica_up` separately if your membership protocol requires it. Replicas with unrelated bases remain unchanged. The caller must select the current incarnation. UUIDs are random, and message arrival order does not establish restart order. Do not call `supersede` with each incoming sync identity: a delayed old sync could remove the current incarnation's entries. The helper returns `Error(CannotSupersedeLocalReplica(local_replica, current_replica))` if the selection would retire the local writer. It cannot change the writer identity of an existing state. For a local restart, create a fresh state with `new_incarnation` and merge peer snapshots into that state. Like `remove_down_replica`, this helper retains causal high-water marks rather than banning future writes from a retired identity. Previously unseen higher clocks can still arrive. Your membership protocol must handle conflicting incarnation claims; `supersede` does not elect a current incarnation. ## Serialization [Section titled “Serialization”](#serialization) Use `lattice_presence/presence_state` for cross-node payloads: ```gleam import lattice_presence/presence_state let payload = presence_state.to_json_string(state) let decoded = presence_state.from_json(payload) ``` The JSON format contains replicated CRDT data: replica name, causal context, clouds, and presence entries. Local replica visibility (`replica_up` / `replica_down`) is intentionally not serialized. Decoding validates causal clock values and limits nested metadata depth so malformed payloads fail as `Error(_)` instead of producing invalid state. Use `presence_state.decoder()` to include state inside an envelope decoder. The former `state_json` import is removed; existing wire payloads keep the same format. # Registers > Using LWWRegister and MVRegister for storing values. Registers are provided by the `lattice_registers` package. If you installed the `lattice_crdt` umbrella, they are already available. Registers store a value rather than a collection. lattice provides two flavors: * `LWWRegister` for single-value, timestamp-based conflict resolution * `MVRegister` for preserving concurrent writes ## LWWRegister (Last-Writer-Wins Register) [Section titled “LWWRegister (Last-Writer-Wins Register)”](#lwwregister-last-writer-wins-register) `LWWRegister(a)` stores a payload, the timestamp of its winning write, and that write's author. ```gleam import lattice_core/replica_id import lattice_registers/lww_register pub fn main() { let local = replica_id.new("node-a") let register = lww_register.new("draft", 1, local) let updated = lww_register.set(register, "published", 2, local) lww_register.value(updated) // -> "published" } ``` `lww_register.set` only applies when the new timestamp is strictly greater than the current timestamp. Labeled calls to `new`, `set`, and `set_with_delta` use `value:`: ```gleam let updated = lww_register.set( register: register, value: "published", timestamp: 2, replica_id: local, ) ``` Every update requires the local writer ID. When upgrading, add that fourth argument to `set` and `set_with_delta`; replace old `set_as` calls with `set` and old `set_as_with_delta` calls with `set_with_delta`. ### Equal-timestamp ties [Section titled “Equal-timestamp ties”](#equal-timestamp-ties) When two replicas merge registers with the same timestamp, lattice resolves the tie deterministically using the replica ID. The register with the lexicographically greater replica ID wins. ```gleam import lattice_core/replica_id import lattice_registers/lww_register pub fn main() { let left = lww_register.new("apple", 10, replica_id.new("node-a")) let right = lww_register.new("zebra", 10, replica_id.new("node-b")) let merged = lww_register.merge(left, right) lww_register.value(merged) // -> "zebra" (node-b > node-a) } ``` This keeps merges deterministic and replica-order independent even when clocks collide. ### Stamping writes from a wall clock [Section titled “Stamping writes from a wall clock”](#stamping-writes-from-a-wall-clock) The strict comparison in `set` is what keeps `merge` commutative, but it makes a wall clock an unsafe timestamp source on its own. A millisecond clock stands still for a millisecond at a time, so two writes inside the same tick carry the same timestamp, and the second one is dropped — even though both came from the same replica and their order is not in doubt. Paint a cell and erase it in the same millisecond, and the erase vanishes. Only the writer knows its two writes are ordered, so the fix belongs on the stamping side: keep the wall clock, but never let it fall behind the timestamp already held. `lww_register.timestamp` reads that back. ```gleam import gleam/int import lattice_registers/lww_register.{type LWWRegister} /// `now` is your own millisecond wall clock. pub fn stamp(register: LWWRegister(String), now: Int) -> Int { int.max(now, lww_register.timestamp(register) + 1) } ``` That is the logical half of a [hybrid logical clock](https://cse.buffalo.edu/tech-reports/2014-04.pdf): the wall clock still drives the value forward, and the `+ 1` fallback keeps successive local writes strictly ordered when it does not move. The same applies across a restart. A client that loads a snapshot must seed its clock from what the snapshot holds, or its first write to a key can lose to a checkpoint written by a replica whose clock ran ahead. Fold `timestamp` over the decoded registers to recover that starting point — no JSON round trip needed. A writer must not reuse one `(timestamp, replica_id)` stamp for different values. Generate a fresh replica ID after a process restart, or persist and restore a logical clock that advances beyond every write made by the reused ID. `lww_register.replica_id` reads back the other half of the metadata: the replica that wrote the value currently held. After a merge that is the replica whose write won, which makes it useful for provenance and for tie-breaking consistently with `merge` in your own code. ## MVRegister (Multi-Value Register) [Section titled “MVRegister (Multi-Value Register)”](#mvregister-multi-value-register) `MVRegister` keeps all concurrent values instead of picking one winner. ```gleam import lattice_core/replica_id import lattice_registers/mv_register pub fn main() { let left = mv_register.new(replica_id.new("node-a")) |> mv_register.set("hello") let right = mv_register.new(replica_id.new("node-b")) |> mv_register.set("world") let merged = mv_register.merge(left, right) mv_register.value(merged) // -> ["hello", "world"] } ``` If one write causally supersedes another, the older value disappears. Multiple values only remain when the writes were concurrent. ## Delta-state mutators [Section titled “Delta-state mutators”](#delta-state-mutators) Registers expose `set_with_delta`: * `lww_register.set_with_delta` * `mv_register.set_with_delta` Each returns both the new register state and a register delta. `MVRegister` deltas carry the new value and the writer's vector clock so remote replicas can remove values causally superseded by the write. See [Delta-State Replication](/advanced/delta-state/) for the shared convention. ## Typed serialization and new authors [Section titled “Typed serialization and new authors”](#typed-serialization-and-new-authors) Both register modules provide `to_json_with(value, encode)` and `from_json_with(input, decoder)` for generic payloads. Existing String codec entry points keep their formats. An adopted LWWRegister retains the author of its winning write. Pass the local writer to `set(register, value, timestamp, local_id)` or `set_with_delta(register, value, timestamp, local_id)` to author a new write. In a map update callback, use the provided `context.replica_id`. # Replica IDs > Identifying nodes in a distributed system. A `ReplicaId` identifies which node performed an operation. It is an opaque wrapper around a string, provided by `lattice_core`. ```gleam import lattice_core/replica_id let node = replica_id.new("node-a") ``` Most CRDTs in lattice require a `ReplicaId` at construction time: * `GCounter`, `PNCounter` — to track per-replica contributions * `LWWRegister` — for deterministic tie-breaking when timestamps are equal * `MVRegister`, `ORSet`, `ORMap` — for causal tagging * `LWWMap` — to identify immutable child assignments * Sequence and text CRDTs — for item identities and local editing clocks A few types do not require one because they have no per-replica state: `GSet` and `TwoPSet`. Choose IDs that are unique across your system and across process incarnations. A stable hostname or node name alone is unsafe after a restart because peers may retain causal history for its previous run. Generate a fresh ID for every process incarnation and keep the stable name separately when other CRDTs need restart-safe identities. ## Unicode ordering and upgrades [Section titled “Unicode ordering and upgrades”](#unicode-ordering-and-upgrades) `replica_id.compare` uses lexicographic UTF-8 byte order on both Erlang and JavaScript. For example, `"\u{e000}"` sorts before `"\u{10000}"`. IDs retain their original strings; the library does not normalize Unicode. Pass well-formed Unicode strings at JavaScript interop boundaries. LWWRegister and modern LWWMap assignments use this order to break equal-timestamp writer ties. ORMap uses it to select between concurrent re-additions with equal generation clocks. Sequence and Fugue use it to order concurrent items and operations, which also affects text and anchor positions. Imported legacy LWWMap entries instead compare their retained String value tie keys in the same UTF-8 order; see the [map migration guidance](/guides/maps/#unicode-ordering-and-upgrades). Older JavaScript versions used UTF-16 order, which reverses some Unicode pairs such as the example above. The correction preserves Erlang's previous order and ASCII ordering. Upgrade peers that exchange affected Unicode IDs together so they use the same conflict-resolution rules. An upgrade cannot restore a register winner that no replica retained. Sequence v2 snapshots can retain their previous item order, and compaction can remove the origins needed to reconstruct it. The comparator correction does not migrate historical snapshots. Fugue computes traversal from its nodes, so existing Unicode-ID siblings can appear in a different order after the upgrade. Plan any historical-state migration separately. ## Sequence and text merges [Section titled “Sequence and text merges”](#sequence-and-text-merges) Both sequence backends and their text wrappers require an explicit output identity: `merge(local, remote, local_id)`. Swapping the states does not change which identity later edits use. `merge_as` remains an equivalent alias. Keep the local ID in your editor or connection context. When combining deltas from one editing operation, use that operation's ID. When receiving remote state or deltas, use the receiving editor's ID. A decoded snapshot still carries the sender's identity until you bind or merge it under a local identity. `sequence.bind(state, local_id)` and `text.bind(state, local_id)` change only editor metadata; they do not rebuild the document or rewrite its historical IDs. The sequence backends expose `replica_id(state)` for reading a state's editing identity. This does not establish ownership of a remote state; your application must still assign a unique ID to each independent writer. ## Recursive maps [Section titled “Recursive maps”](#recursive-maps) Bind a received map to the local writer before editing its children. This includes keys that existed only on the sender and maps nested inside other maps. Preserve historical item IDs and winning write authors; rebinding selects the identity for subsequent operations. ORMap scopes child editing identities by the enclosing key path and generation. Removing and re-adding a key creates a fresh generation, so the same writer does not restart the old leaf's item namespace. LWWMap child replacements use the new outer write identity as part of their scope. Do not alter an LWWRegister's stored author when adopting it. Pass the local identity to every `lww_register.set` or `set_with_delta` call. A writer must not reuse one timestamp for different values. After a restart, use a fresh replica ID or restore a durable logical clock beyond every write made by that ID. ## Presence incarnations [Section titled “Presence incarnations”](#presence-incarnations) `lattice_presence` provides this pattern directly: ```gleam import lattice_presence/presence_state let presence = presence_state.new_incarnation("node-a") ``` `new_incarnation` preserves `node-a` as the stable base while generating a unique identity for the current process. Use `base_replica` when you need the stable name, and use `same_base` to compare incarnation identities by that name. # Sets > Using GSet, TwoPSet, and ORSet for distributed collections. Sets are provided by the `lattice_sets` package. If you installed the `lattice_crdt` umbrella, they are already available. Use sets when replicas need to track membership and later merge without coordination. ## GSet (Grow-only Set) [Section titled “GSet (Grow-only Set)”](#gset-grow-only-set) `GSet` is a grow-only set. Elements can be added but never removed. Merge is a set union. ```gleam import lattice_sets/g_set pub fn main() { let set_a = g_set.new() |> g_set.add("apple") |> g_set.add("banana") let set_b = g_set.new() |> g_set.add("banana") |> g_set.add("cherry") let merged = g_set.merge(set_a, set_b) g_set.contains(merged, "apple") // -> True g_set.contains(merged, "cherry") // -> True } ``` `GSet` requires no `ReplicaId` because it does not track causal history. ## TwoPSet (Two-Phase Set) [Section titled “TwoPSet (Two-Phase Set)”](#twopset-two-phase-set) `TwoPSet` supports both add and remove, but removal is permanent. Once an element is removed, it can never be re-added — the tombstone is irrevocable. ```gleam import lattice_sets/two_p_set pub fn main() { let set = two_p_set.new() |> two_p_set.add("apple") |> two_p_set.add("banana") |> two_p_set.remove("banana") two_p_set.contains(set, "apple") // -> True two_p_set.contains(set, "banana") // -> False (tombstoned) // Re-adding has no effect let re_added = two_p_set.add(set, "banana") two_p_set.contains(re_added, "banana") // -> False } ``` Like `GSet`, `TwoPSet` requires no `ReplicaId`. ## ORSet (Observed-Remove Set) [Section titled “ORSet (Observed-Remove Set)”](#orset-observed-remove-set) `ORSet` is the most flexible set CRDT. It supports add, remove, and re-add. Each add operation creates a unique causal tag, and remove only deletes tags that the removing replica has observed. This gives **add-wins** semantics: a concurrent add on another replica survives a remove. `ORSet` requires a `ReplicaId` from `lattice_core` to track causal history. ```gleam import lattice_core/replica_id import lattice_sets/or_set pub fn main() { let node_a = or_set.new(replica_id.new("node-a")) |> or_set.add("apple") let node_b = or_set.new(replica_id.new("node-b")) |> or_set.add("apple") |> or_set.remove("apple") let merged = or_set.merge(node_a, node_b) or_set.contains(merged, "apple") // -> True (node-a's add was concurrent with node-b's remove) } ``` Unlike `TwoPSet`, elements can be re-added after removal: ```gleam import lattice_core/replica_id import lattice_sets/or_set pub fn main() { let set = or_set.new(replica_id.new("demo")) |> or_set.add("x") |> or_set.remove("x") |> or_set.add("x") or_set.contains(set, "x") // -> True } ``` ## Choosing the right set [Section titled “Choosing the right set”](#choosing-the-right-set) * **`GSet`** — when you never need to remove elements. Simplest, no causal tracking overhead. * **`TwoPSet`** — when you need one-time removal. Still no causal tracking, but removed elements can never come back. * **`ORSet`** — when you need full add/remove/re-add flexibility. Requires a replica ID and maintains causal metadata. ## Delta-state mutators [Section titled “Delta-state mutators”](#delta-state-mutators) Sets expose `*_with_delta` variants for incremental replication: * `g_set.add_with_delta` * `two_p_set.add_with_delta` * `two_p_set.remove_with_delta` * `or_set.add_with_delta` * `or_set.remove_with_delta` Each returns both the new set state and a compact set delta. OR-Set deltas carry the changed causal tags or tombstones needed for remote replicas to converge. See [Delta-State Replication](/advanced/delta-state/) for the shared convention. ## Typed serialization [Section titled “Typed serialization”](#typed-serialization) All set modules provide `to_json_with(value, encode)` and `from_json_with(input, decoder)` for integer, record, and tagged-union payloads. Existing String codecs retain their formats. Generic ORSet uses version 3 with value/tag entries instead of arbitrary JSON object keys. Its generic decoder accepts that version; the String decoder retains version 1/2 support. Both paths preserve causal history, not just visible members. # Installation > How to install lattice in your Gleam project. ## Umbrella package (recommended) [Section titled “Umbrella package (recommended)”](#umbrella-package-recommended) Install `lattice_crdt` to get every CRDT in one dependency: ```sh gleam add lattice_crdt ``` Even with the umbrella, imports use the sub-package names: ```gleam import lattice_core/replica_id import lattice_counters/g_counter import lattice_registers/lww_register import lattice_maps/or_map ``` Install `lattice_presence` separately when you need distributed presence: ```sh gleam add lattice_presence ``` ## Individual packages [Section titled “Individual packages”](#individual-packages) If you only need one category of CRDT, depend on that package directly: ```sh gleam add lattice_counters ``` Transitive dependencies are pulled in automatically. For example, `lattice_counters` depends on `lattice_core`, so you can import `lattice_core/replica_id` without adding `lattice_core` explicitly. See [Packages](/packages/) for the full list and dependency diagram. ## When to choose which [Section titled “When to choose which”](#when-to-choose-which) * **`lattice_crdt`** — getting started, prototyping, or using CRDTs from multiple categories. * **`lattice_presence`** — topic/key/pid presence tracking with metadata and replica visibility. * **Individual packages** — production deployments where you want to minimize dependency count or binary size. ## Target runtimes [Section titled “Target runtimes”](#target-runtimes) All packages target both Erlang and JavaScript runtimes, so the same API works across both Gleam targets. After installing, continue with the [Quick Start](/quick-start/) or browse the [guides](/guides/counters/). # What is lattice? > An introduction to lattice and CRDTs. `lattice` is a Gleam library for **Conflict-free Replicated Data Types** (CRDTs). ## What is a CRDT? [Section titled “What is a CRDT?”](#what-is-a-crdt) A CRDT is a data structure that can be replicated across multiple nodes, updated independently on each node without coordination, and merged back together with a guarantee: all replicas converge to the same state. No consensus protocol, no locking, no conflict resolution callbacks — the math of the data structure itself ensures convergence. For example, a grow-only counter lets every replica increment locally. When two replicas merge, they take the per-replica maximum and sum the result. No matter what order merges happen in, the final value is always the same. ## What you get [Section titled “What you get”](#what-you-get) The library is organized across [focused packages](/packages/): * **counters** (`lattice_counters`): GCounter (Grow-only Counter), PNCounter (Positive-Negative Counter) * **registers** (`lattice_registers`): LWWRegister (Last-Writer-Wins Register), MVRegister (Multi-Value Register) * **sets** (`lattice_sets`): GSet (Grow-only Set), TwoPSet (Two-Phase Set), ORSet (Observed-Remove Set) * **maps** (`lattice_maps`): LWWMap (Last-Writer-Wins Map), ORMap (Observed-Remove Map) * **presence** (`lattice_presence`): distributed topic/key/pid presence tracking * **causal infrastructure** (`lattice_core`): `ReplicaId`, `VersionVector`, `DotContext` The umbrella package `lattice_crdt` depends on the core CRDT packages, so a single `gleam add lattice_crdt` gives you counters, registers, sets, maps, and causal infrastructure. Add `lattice_presence` separately when you need presence tracking. Each CRDT module follows the same basic shape: * `new` to create an empty or initial value * mutators such as `increment`, `set`, `add`, or `remove` * `merge` to combine state from replicas * `value` to read the user-facing value * `to_json` / `from_json` for serialization Many state-changing functions also expose `*_with_delta` companions. These return both the new state and a compact delta that remote replicas merge with the same CRDT semantics. See [Delta-State Replication](/advanced/delta-state/) for details. ## Choosing the right CRDT [Section titled “Choosing the right CRDT”](#choosing-the-right-crdt) * Use **counters** for totals that must converge across replicas. * Use **registers** for single values, with either last-writer-wins or multi-value conflict handling. * Use **sets** for membership tracking. * Use **maps** when each key needs its own convergent value. * Use **presence** when you need topic/key membership with metadata and local replica visibility. If you are new to the library, start with the [Quick Start](/quick-start/). # Package Structure > How lattice is organized into focused packages. lattice is organized as a family of focused packages for CRDTs and their shared infrastructure. You can depend on the `lattice_crdt` umbrella for the core toolkit, or pick individual packages for a smaller dependency graph. ## Packages [Section titled “Packages”](#packages) | Package | Version | Docs | What it provides | | -------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------- | | `lattice_crdt` | [![](https://img.shields.io/hexpm/v/lattice_crdt)on Hex](https://hex.pm/packages/lattice_crdt) | [API docs](https://hexdocs.pm/lattice_crdt/) | Umbrella for core, counters, registers, sets, maps, sequence, and text | | `lattice_core` | [![](https://img.shields.io/hexpm/v/lattice_core)on Hex](https://hex.pm/packages/lattice_core) | [API docs](https://hexdocs.pm/lattice_core/) | `ReplicaId`, `VersionVector`, `DotContext` — shared causal infrastructure | | `lattice_counters` | [![](https://img.shields.io/hexpm/v/lattice_counters)on Hex](https://hex.pm/packages/lattice_counters) | [API docs](https://hexdocs.pm/lattice_counters/) | `GCounter`, `PNCounter` | | `lattice_registers` | [![](https://img.shields.io/hexpm/v/lattice_registers)on Hex](https://hex.pm/packages/lattice_registers) | [API docs](https://hexdocs.pm/lattice_registers/) | `LWWRegister`, `MVRegister` | | `lattice_sets` | [![](https://img.shields.io/hexpm/v/lattice_sets)on Hex](https://hex.pm/packages/lattice_sets) | [API docs](https://hexdocs.pm/lattice_sets/) | `GSet`, `TwoPSet`, `ORSet` | | `lattice_maps` | [![](https://img.shields.io/hexpm/v/lattice_maps)on Hex](https://hex.pm/packages/lattice_maps) | [API docs](https://hexdocs.pm/lattice_maps/) | `LWWMap`, `ORMap`, `Crdt` dispatch | | `lattice_sequence` | [![](https://img.shields.io/hexpm/v/lattice_sequence)on Hex](https://hex.pm/packages/lattice_sequence) | [API docs](https://hexdocs.pm/lattice_sequence/) | Generic ordered-list CRDT with move support | | `lattice_fugue` | Unreleased | Not published yet | Non-interleaving sequence CRDT implementing Fugue | | `lattice_text_core` | [![](https://img.shields.io/hexpm/v/lattice_text_core)on Hex](https://hex.pm/packages/lattice_text_core) | [API docs](https://hexdocs.pm/lattice_text_core/) | Shared grapheme and range helpers for text CRDTs | | `lattice_text` | [![](https://img.shields.io/hexpm/v/lattice_text)on Hex](https://hex.pm/packages/lattice_text) | [API docs](https://hexdocs.pm/lattice_text/) | Plain-text CRDT backed by `lattice_sequence` | | `lattice_text_fugue` | Unreleased | Not published yet | Non-interleaving text CRDT backed by `lattice_fugue` | | `lattice_presence` | [![](https://img.shields.io/hexpm/v/lattice_presence)on Hex](https://hex.pm/packages/lattice_presence) | [API docs](https://hexdocs.pm/lattice_presence/) | Distributed presence CRDT with topic/key/pid/meta tracking | * `lattice_crdt` * Version [![](https://img.shields.io/hexpm/v/lattice_crdt)View `lattice_crdt` on Hex](https://hex.pm/packages/lattice_crdt) * Docs [Read `lattice_crdt` API docs](https://hexdocs.pm/lattice_crdt/) * Provides Umbrella for core, counters, registers, sets, maps, sequence, and text * `lattice_core` * Version [![](https://img.shields.io/hexpm/v/lattice_core)View `lattice_core` on Hex](https://hex.pm/packages/lattice_core) * Docs [Read `lattice_core` API docs](https://hexdocs.pm/lattice_core/) * Provides `ReplicaId`, `VersionVector`, `DotContext` — shared causal infrastructure * `lattice_counters` * Version [![](https://img.shields.io/hexpm/v/lattice_counters)View `lattice_counters` on Hex](https://hex.pm/packages/lattice_counters) * Docs [Read `lattice_counters` API docs](https://hexdocs.pm/lattice_counters/) * Provides `GCounter`, `PNCounter` * `lattice_registers` * Version [![](https://img.shields.io/hexpm/v/lattice_registers)View `lattice_registers` on Hex](https://hex.pm/packages/lattice_registers) * Docs [Read `lattice_registers` API docs](https://hexdocs.pm/lattice_registers/) * Provides `LWWRegister`, `MVRegister` * `lattice_sets` * Version [![](https://img.shields.io/hexpm/v/lattice_sets)View `lattice_sets` on Hex](https://hex.pm/packages/lattice_sets) * Docs [Read `lattice_sets` API docs](https://hexdocs.pm/lattice_sets/) * Provides `GSet`, `TwoPSet`, `ORSet` * `lattice_maps` * Version [![](https://img.shields.io/hexpm/v/lattice_maps)View `lattice_maps` on Hex](https://hex.pm/packages/lattice_maps) * Docs [Read `lattice_maps` API docs](https://hexdocs.pm/lattice_maps/) * Provides `LWWMap`, `ORMap`, `Crdt` dispatch * `lattice_sequence` * Version [![](https://img.shields.io/hexpm/v/lattice_sequence)View `lattice_sequence` on Hex](https://hex.pm/packages/lattice_sequence) * Docs [Read `lattice_sequence` API docs](https://hexdocs.pm/lattice_sequence/) * Provides Generic ordered-list CRDT with move support * `lattice_fugue` * Version Unreleased * Docs Available after the first release * Provides Non-interleaving sequence CRDT implementing Fugue * `lattice_text_core` * Version [![](https://img.shields.io/hexpm/v/lattice_text_core)View `lattice_text_core` on Hex](https://hex.pm/packages/lattice_text_core) * Docs [Read `lattice_text_core` API docs](https://hexdocs.pm/lattice_text_core/) * Provides Shared grapheme and range helpers for text CRDTs * `lattice_text` * Version [![](https://img.shields.io/hexpm/v/lattice_text)View `lattice_text` on Hex](https://hex.pm/packages/lattice_text) * Docs [Read `lattice_text` API docs](https://hexdocs.pm/lattice_text/) * Provides Plain-text CRDT backed by `lattice_sequence` * `lattice_text_fugue` * Version Unreleased * Docs Available after the first release * Provides Non-interleaving text CRDT backed by `lattice_fugue` * `lattice_presence` * Version [![](https://img.shields.io/hexpm/v/lattice_presence)View `lattice_presence` on Hex](https://hex.pm/packages/lattice_presence) * Docs [Read `lattice_presence` API docs](https://hexdocs.pm/lattice_presence/) * Provides Distributed presence CRDT with topic/key/pid/meta tracking ## Dependencies [Section titled “Dependencies”](#dependencies) ``` graph LR lattice_counters --> lattice_core lattice_fugue --> lattice_core lattice_registers --> lattice_core lattice_sequence --> lattice_core lattice_sets --> lattice_core lattice_maps --> lattice_core lattice_maps --> lattice_counters lattice_maps --> lattice_registers lattice_maps --> lattice_sequence lattice_maps --> lattice_sets lattice_maps --> lattice_text lattice_text --> lattice_core lattice_text --> lattice_sequence lattice_text --> lattice_text_core lattice_crdt --> lattice_core lattice_crdt --> lattice_counters lattice_crdt --> lattice_registers lattice_crdt --> lattice_sequence lattice_crdt --> lattice_sets lattice_crdt --> lattice_maps lattice_crdt --> lattice_text lattice_text_fugue --> lattice_core lattice_text_fugue --> lattice_fugue lattice_text_fugue --> lattice_text_core lattice_presence ``` The only packages with no lattice package dependencies are `lattice_core`, `lattice_text_core`, and `lattice_presence`. `lattice_text_core` depends only on `gleam_stdlib`; `lattice_core` and `lattice_presence` also depend on `gleam_json`. ## Import paths [Section titled “Import paths”](#import-paths) In Gleam, imports come from the package name. Even when you install the umbrella `lattice_crdt`, you import from the sub-package names: ```gleam import lattice_core/replica_id import lattice_counters/g_counter import lattice_sets/or_set import lattice_maps/or_map import lattice_sequence/sequence import lattice_fugue/sequence import lattice_text/text import lattice_text_core/grapheme import lattice_text_fugue/text import lattice_presence/presence_state ``` ## Which approach to choose [Section titled “Which approach to choose”](#which-approach-to-choose) **Start with `lattice_crdt`** if you are getting started, prototyping, or using CRDTs from multiple categories. It includes the causal core, counters, registers, sets, maps, the YATA-style sequence, and its text wrapper. **Pick individual packages** when binary size or dependency count matters, or when you only need one category of CRDT. For example, if you only need counters: ```sh gleam add lattice_counters ``` Add `lattice_presence` separately when you need distributed presence. It has no lattice package dependencies and is not included in the `lattice_crdt` umbrella. ### Evaluating the Fugue packages [Section titled “Evaluating the Fugue packages”](#evaluating-the-fugue-packages) `lattice_fugue` and `lattice_text_fugue` are not published to Hex yet. To evaluate either package, clone the lattice repository and declare the package as a local path dependency in your project's `gleam.toml`: ```toml [dependencies] lattice_fugue = { path = "../lattice/packages/lattice_fugue" } lattice_text_fugue = { path = "../lattice/packages/lattice_text_fugue" } ``` Keep only the dependency or dependencies you need. Paths are relative to your project's `gleam.toml`, so adjust them for the location of your lattice clone. Do not use `gleam add` for these unreleased packages. See [Installation](/installation/) for full details. # Quick Start > Get up and running with lattice in minutes. Install the umbrella package: ```sh gleam add lattice_crdt ``` This gives you the core CRDT packages. See [Installation](/installation/) for individual package options, including distributed presence. ## Merge counters from multiple replicas [Section titled “Merge counters from multiple replicas”](#merge-counters-from-multiple-replicas) `GCounter` is the simplest place to start: each replica can only increase its own contribution, and merging takes the per-replica maximum. Save this as `src/quick_start.gleam`: On narrow screens, scroll code blocks horizontally to read each full line. ```gleam import gleam/int import gleam/io import lattice_core/replica_id import lattice_counters/g_counter pub fn main() { let assert Ok(counter_a) = g_counter.new(replica_id.new("node-a")) |> g_counter.increment(2) let assert Ok(counter_b) = g_counter.new(replica_id.new("node-b")) |> g_counter.increment(3) let merged = g_counter.merge(counter_a, counter_b) merged |> g_counter.value |> int.to_string |> io.println } ``` Run the example from your project directory: ```sh gleam run -m quick_start ``` Expected output: ```text 5 ``` **Success:** you now have a convergent counter: two replicas updated independently, then merged into the same combined value. `g_counter.increment` returns `Result` and rejects negative deltas with `Error(NegativeDelta(delta))`. If you need a counter that can go down, use `pn_counter` from `lattice_counters`; both `increment` and `decrement` still take non-negative deltas because the underlying state is grow-only. For incremental replication, counters also expose `*_with_delta` mutators that return `Ok(#(new_state, delta))` or an error. See [Delta-State Replication](/advanced/delta-state/). ## Advanced recipes [Section titled “Advanced recipes”](#advanced-recipes) Open these when you are ready to move beyond the first convergent counter. Each recipe has a stable fragment link: [LWW register](#lww-register-recipe) and [OR-Map](#or-map-recipe). Advanced recipeResolve ties deterministically with LWW registersChoose a value by timestamp, with replica IDs breaking equal-timestamp ties. `LWWRegister` stores a value plus a timestamp. Newer timestamps win. If two replicas write at the same timestamp, lattice breaks the tie deterministically using the replica ID. ```gleam import lattice_core/replica_id import lattice_registers/lww_register pub fn main() { let left = lww_register.new("apple", 7, replica_id.new("node-a")) let right = lww_register.new("zebra", 7, replica_id.new("node-b")) let merged = lww_register.merge(left, right) lww_register.value(merged) // -> "zebra" (node-b > node-a lexicographically) } ``` `lww_register.set` only applies when the new timestamp is strictly greater than the current one. Advanced recipeStore CRDT values inside an OR-MapTrack add-wins keys while merging a nested CRDT value for each key. `ORMap(a)` tracks keys with add-wins semantics within a generation and stores a CRDT child at each key. `CrdtSpec(a)` determines its initial value. Re-adding a removed key starts a fresh generation; the newer generation wins over older content. ```gleam import lattice_core/replica_id import lattice_counters/g_counter import lattice_maps/crdt import lattice_maps/or_map fn add_points(value: crdt.Crdt(String), delta: Int) -> crdt.Crdt(String) { case value { crdt.CrdtGCounter(counter) -> { let assert Ok(counter) = g_counter.increment(counter, delta) crdt.CrdtGCounter(counter) } other -> other } } pub fn main() { let assert Ok(scoreboard_a) = or_map.new(replica_id.new("node-a"), crdt.GCounterSpec) |> or_map.update("alice", fn(value) { add_points(value, 2) }) let assert Ok(scoreboard_b) = or_map.new(replica_id.new("node-b"), crdt.GCounterSpec) |> or_map.update("alice", fn(value) { add_points(value, 3) }) let assert Ok(merged) = or_map.merge(scoreboard_a, scoreboard_b) case or_map.get(merged, "alice") { Ok(crdt.CrdtGCounter(counter)) -> g_counter.value(counter) _ -> 0 } // -> 5 } ``` `or_map.update` returns `Error(TypeMismatch(...))` if the callback returns a different CRDT variant from the map's specification. The examples use known positive amounts; handle counter errors before updating a map when amounts come from external input. For more detail, see the [Counters guide](/guides/counters/), [Registers guide](/guides/registers/), [Sets guide](/guides/sets/), and [Maps guide](/guides/maps/). For topic/key presence tracking, see the [Presence guide](/guides/presence/).