[type | entity_len | entity | row_id_len | row_id | payload], and 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 — no separate install.
Declare CRDT-backed fields
In your schema: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:Saving and loading
The engine handles persistence transparently — 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 — 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 surface — 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 doesn’t care what’s inside payload — that’s Loro’s binary format. To peek/decode 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 NOT to use CRDTs
CRDTs are great for collaborative state where two users might edit at the same time. They’re overkill 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, but for very long-lived docs, periodically save a full snapshot to bound the size of the update history. The engine handles this automatically.
- Garbage collection — Loro tracks tombstones for deletes;
doc.compact()reclaims memory after large deletions. - Presence cleanup — set a TTL on presence entries; users who close the tab without explicitly leaving will linger otherwise.
Examples
examples/collaborative-doc— minimal CRDT-backed text editorexamples/forge— design-tool-style multi-cursor editingexamples/linear— issue tracker with CRDT-backed comment threads