[type | entity_len | entity | row_id_len | row_id | payload]. The receiving side feeds the payload to a per-row LoroDoc that converges automatically.
What you get
- Collaborative text: multiple cursors, conflict-free inserts/deletes, undo/redo
- Collaborative lists: append, insert, move, delete with stable item ids
- Collaborative maps: key-value with last-writer-wins per key
- Collaborative trees: hierarchical structures (outlines, file trees)
- Counter: multi-writer increment/decrement that always converges
- Cursor positions: anchor a cursor to a position that survives concurrent edits
Install
PylonSync, with no separate install.
Declare CRDT-backed fields
In your schema:.crdt(annotation) on a field builder. "text" and "counter" are wired end-to-end today. "list", "movable-list", and "tree" are reserved (the wire format is locked in, the server-side projection is in progress). You can still edit list, map, and tree containers on the per-row LoroDoc (see below), because the server never interprets CRDT payloads.
The Pylon server stores CRDT fields as opaque bytes and does not interpret them. Clients decode the bytes into Loro containers and edit them locally. Updates broadcast to other subscribers via the binary WebSocket channel.
Subscribe and edit
useLoroDoc subscribes the engine to binary updates for (entity, rowId), decodes incoming frames, and returns the live LoroDoc. Edits to the doc dispatch through Loro’s local state; the hook ships the resulting binary update back over the WebSocket.
Multiple useLoroDoc callers on the same row share one subscription (refcounted). Opening the same document in two tabs doesn’t double-subscribe.
Container types
Text — collaborative rich text
List — append / insert / move
Map — key-value
Counter — multi-writer increment
Tree — hierarchical
Cursors
For “show where each user is editing”:engine.setPresence({ cursor: myCursor }), you get the multiplayer-cursor effect.
Awareness / presence
For lightweight ephemeral state (who’s editing, where their cursor is, what they’re typing) that doesn’t need to be persisted, use theuseRoom hook from @pylonsync/react. It returns the live peer list plus a setPresence you call as the local cursor moves:
peer.presence is the last object that peer pushed via setPresence. Presence rides the same WebSocket as CRDT updates but doesn’t go into the CRDT itself. It’s transient. Peers who disconnect drop out of room.peers.
Saving and loading
The engine handles persistence for you. Every CRDT update is broadcast, and the server holds the canonical state. To get the current snapshot for a one-off use (export, share, backup):Undo / redo
Loro has built-in version vectors that make undo/redo work correctly even with concurrent edits:Performance
- Local edits are O(log n) in the size of the document. They stay fast even for large texts.
- Binary frames are tiny. Typical updates are a few dozen bytes. Snapshots are larger but only sent on subscribe.
- CRDT logic is Rust. Both
loro-crdt(JS) andloro-swiftwrap the same Rust core, so convergence is identical and performance is consistent. - No conflict markers ever appear. Concurrent edits always merge cleanly.
Wire format
Frame layout (matchescrates/router/src/lib.rs::encode_crdt_frame and packages/loro/src/wire.ts):
0x10 = full snapshot, 0x11 = incremental update.
The engine does not read payload. That is Loro’s binary format. To decode it for debugging:
Swift
Same model. The Swift bridge:PylonLoroDoc.attach(to:) registers the binary handler with the engine and sends the crdt-subscribe message. Detach (or let the doc go out of scope) to unsubscribe.
When to skip CRDTs
CRDTs work well for collaborative state, where two users might edit at the same time. They are unnecessary for:- Single-user data: use a normal entity field
- Server-authoritative state: orders, payments, anything where the server is the source of truth and last-write-wins is correct
- High-frequency telemetry: CRDTs aren’t designed for write-heavy event streams; use a regular entity with append semantics
Document.title can be a normal string while Document.body is a LoroText.
Production checklist
- Snapshot frequency: Loro’s hybrid logical clock works well. For very long-lived docs, periodically save a full snapshot to bound the size of the update history. The engine does this automatically.
- Garbage collection: Loro tracks tombstones for deletes.
doc.compact()reclaims memory after large deletions. - Presence cleanup: set a TTL on presence entries. Otherwise, users who close the tab without leaving will linger.
Examples
examples/pad— CRDT-backed collaborative markdown editorexamples/forge— design-tool-style multi-cursor editingexamples/linear— issue tracker with CRDT-backed comment threads