Overview
vectis-crdt is an open-source (MIT, v0.2.0) conflict-free replicated data type for the browser. It implements an RGA/YATA ordered sequence with per-object LWW registers, compiled from Rust to WebAssembly (wasm32-unknown-unknown). Every peer edits locally without coordination; deltas are exchanged as compact binary patches, and a deterministic merge engine guarantees that all peers converge to the same state — regardless of the order in which operations arrive.
Conflict-free by construction. Zero backend for merge logic.
16 KB
WASM binary
0
merge conflicts
O(1)
convergence per op
Δ sync
delta-only wire format
Built for collaborative edges
Zero conflicts
Deterministic merge. Two peers always reach the same state, regardless of operation order. CRDT · deterministic
Delta sync
Only send what changed. Binary wire format keeps bandwidth minimal. delta · binary
Offline-first
Work offline, sync when reconnected. Vector clocks track causality. offline · vector clocks
Rich attributes
Per-object LWW registers for color, width, opacity, transform — any mutable property. LWW · properties
WebAssembly
Runs in the browser at near-native speed. 16 KB binary, no WASM runtime needed. wasm · no_std
Ordered sequence
RGA/YATA concurrent insert converges without coordination. RGA · YATA
How it works
Three steps, no conflicts:
- EDIT — produce an operation locally.
insert(pos, obj) → RGA/YATA: a local op, no coordination with other peers. - SYNC — exchange deltas with peers.
doc.delta() → binary bytes: only the changed ops travel, keeping the wire minimal. - CONVERGE — deterministic merge, no conflicts.
peer.apply(delta) → same state: the vector clock enforces causal order.
Runtime topology
Every edit flows through seven stages, in one pass:
Operation → RGA/YATA → LWW Reg → Vector Clock → Delta Sync → Merge Engine → State
| # | Node | Role | Detail |
|---|---|---|---|
| 01 | Operation | insert · delete | Local op produced without coordination |
| 02 | RGA/YATA | sequence CRDT | Replicated Growable Array, YATA variant |
| 03 | LWW Reg | color · width · opacity | Last-Write-Wins per-property register |
| 04 | Vector Clock | causality | Tracks causal ordering across peers |
| 05 | Delta Sync | binary diff | Binary-encoded minimal changeset |
| 06 | Merge Engine | convergence | Deterministic CRDT merge algorithm |
| 07 | State | consistent view | Final consistent document state |
The four merge stages
Four stages, one pass — each with a bounded cost:
| Stage | Structure | Cost | What it does |
|---|---|---|---|
| 01 · RGA/YATA | ordered sequence | O(1) | Ordered sequence insert. Concurrent inserts at the same position converge deterministically without coordination. |
| 02 · LWW Register | per-property register | O(1) | Last-write-wins properties. Concurrent writes to the same property are resolved by timestamp. |
| 03 · Vector Clock | per-peer counters | O(peers) | Causal ordering. Each peer's clock entry tracks what it has seen, so causally-related ops apply in order. |
| 04 · Delta Merge | binary patch | O(Δ) | Binary patch apply. A delta from peer A or B carries only its ins / del / prop / ack ops. |
Core CRDT ideas
RGA/YATA — the ordered sequence
The document body is a Replicated Growable Array using the YATA variant of the algorithm. Inserts and deletes are appended to a log; deleted items are tombstoned in the RGA log rather than removed, which is what guarantees convergence when a delete races a concurrent operation on the same element.
LWW registers — mutable properties
Each object in the sequence carries per-property Last-Write-Wins registers (color, width, opacity, transform — any mutable property). When two peers write the same property concurrently, the write with the later timestamp wins on every peer, deterministically.
Vector clocks — causality
A vector clock tracks the causal ordering of operations across peers. It is what lets a peer work offline and, on reconnect, exchange exactly the operations the other side has not seen. Clock comparison costs O(peers).
Convergence
The merge engine is deterministic: given the same set of operations, in any delivery order, every replica computes the identical final state. There is no conflict-resolution callback, no server-side arbitration — zero backend for merge logic.
Install
vectis-crdt ships as an npm package wrapping the WebAssembly build, and as a Rust crate.
install · shellnpm install vectis-crdt
# or
pnpm add vectis-crdt
yarn add vectis-crdt
rust · cargo
cargo add vectis-crdt
wasm32-unknown-unknown. No separate WASM runtime is needed — the 16 KB binary is loaded by the JS wrapper itself.Quick start
Up in under a minute: create a document, insert two shapes, export a delta, and apply it on a second peer.
index.ts · typescriptimport { VectisDoc } from "vectis-crdt";
const doc = await VectisDoc.create();
doc.insert(0, { id: "shape-1", color: "#c0432c", width: 2 });
doc.insert(1, { id: "shape-2", color: "#2e4a6b", width: 1 });
const delta = doc.delta();
const peer = await VectisDoc.create();
peer.apply(delta);
console.log(peer.state());
VectisDoc.create() must be awaited before any other call — it initialises the WebAssembly engine. After that, the entire JS surface is thin and synchronous.API reference
vectis-crdt exposes a single class, VectisDoc, that wraps the WebAssembly CRDT engine. All merge logic runs in the WASM sandbox; the JS surface is thin and synchronous.
Document operations at a glance
| Method | Description | Example |
|---|---|---|
doc.insert(pos, obj) | Insert object at position | doc.insert(0, { color: "#f00" }) |
doc.delete(id) | Remove object by ID | doc.delete("obj-42") |
doc.setProp(id, key, val) | Set LWW property | doc.setProp("obj-1", "color", "#00f") |
doc.delta() | Export changes as binary | const bytes = doc.delta() |
doc.apply(delta) | Merge remote delta | doc.apply(remoteDelta) |
Methods
static VectisDoc.create() → Promise<VectisDoc>
Initialises the WebAssembly CRDT engine and returns a ready document instance. Must be awaited before any other call.
import { VectisDoc } from "vectis-crdt";
const doc = await VectisDoc.create();
console.log("VectisDoc ready");
doc.insert(pos: number, obj: object) → void
Inserts an object at the given position in the RGA/YATA ordered sequence. Concurrent inserts at the same position converge deterministically.
doc.insert(0, { id: "shape-1", color: "#c0432c", width: 2 });
doc.insert(1, { id: "shape-2", color: "#2e4a6b", width: 1 });
doc.delete(id: string) → void
Removes an object by its ID from the sequence. Tombstoned in the RGA log to ensure convergence with concurrent operations.
doc.delete("shape-1");
doc.setProp(id: string, key: string, value: unknown) → void
Sets a per-object LWW (Last-Write-Wins) register property. Concurrent writes to the same property are resolved by timestamp.
doc.setProp("shape-1", "color", "#00f");
doc.setProp("shape-1", "width", 4);
doc.delta() → Uint8Array
Exports all new operations since the last delta() call as a compact binary patch. Send this over the network to sync with peers.
const bytes = doc.delta();
// send bytes over WebSocket, WebRTC, etc.
ws.send(bytes);
doc.apply(delta: Uint8Array) → void
Applies a binary delta received from a peer. Deterministically merges the operations; the document converges to the same state on all peers.
ws.onmessage = (e) => {
doc.apply(new Uint8Array(e.data));
render(doc.state());
};
Delta sync & wire format
vectis-crdt never ships full documents over the network. doc.delta() exports only the operations produced since the last export, as a compact binary patch (Uint8Array) — the transport is yours: WebSocket, WebRTC, or anything that moves bytes.
- Delta-only — only changed ops travel; the wire stays minimal.
- Binary encoding — the changeset is binary-encoded, not JSON, keeping bandwidth minimal.
- Op kinds — a delta carries
ins(insert),del(delete),prop(LWW write), andackentries, together with the sender's vector clock. - New peers — a peer that has never synced simply receives the full log as its first delta; from then on, exchanges are incremental.
- Idempotent, order-tolerant merge —
doc.apply()uses the vector clock to place remote ops in causal order, so deltas can arrive late, duplicated, or interleaved and every replica still converges.
Guides & recipes
Get vectis-crdt running in minutes — from a two-peer offline merge to a live multi-peer canvas.
Basic two-peer example
This example shows two peers editing the same document offline and converging on reconnect.
two-peer merge · javascriptimport { VectisDoc } from 'vectis-crdt';
const alice = await VectisDoc.create('alice');
const bob = await VectisDoc.create('bob');
// Both insert elements concurrently (offline)
const opA = alice.insert('circle-1', { after: 'root' });
const opB = bob.insert('rect-1', { after: 'root' });
// Set properties
const opAColor = alice.setProp('circle-1', 'color', '#c0432c');
const opBColor = bob.setProp('rect-1', 'color', '#2e4a6b');
// Reconnect — exchange all ops
alice.apply(opB);
alice.apply(opBColor);
bob.apply(opA);
bob.apply(opAColor);
// Both converge to identical state
console.log(alice.toArray()); // same order on both peers
console.log(bob.toArray()); // identical
WebSocket integration
Connect multiple browser peers via WebSocket for real-time collaboration.
websocket relay · javascriptimport { VectisDoc } from 'vectis-crdt';
const peerId = crypto.randomUUID();
const doc = await VectisDoc.create(peerId);
const ws = new WebSocket('wss://your-relay.example.com');
// On connect, send our current state as delta
ws.onopen = () => {
const delta = doc.delta(); // full log for new peers
ws.send(JSON.stringify({ type: 'sync', ops: Array.from(delta.ops) }));
};
// Apply remote ops
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === 'op') doc.apply(msg.op);
if (msg.type === 'sync') doc.applyDelta({
ops: new Uint8Array(msg.ops),
clock: msg.clock
});
};
// Local insert — broadcast to all peers
function addShape(id, afterId) {
const op = doc.insert(id, { after: afterId });
ws.send(JSON.stringify({ type: 'op', op }));
render(doc.toArray());
}
React example
CollabCanvas.jsx · reactimport { useEffect, useState } from 'react';
import { VectisDoc } from 'vectis-crdt';
export function CollabCanvas() {
const [doc, setDoc] = useState(null);
const [elements, setElements] = useState([]);
useEffect(() => {
VectisDoc.create('peer-' + Date.now()).then(d => {
setDoc(d);
setElements(d.toArray());
});
}, []);
function addCircle() {
const id = 'circle-' + Date.now();
doc.insert(id, { after: 'root' });
doc.setProp(id, 'color', '#c0432c');
setElements([...doc.toArray()]);
}
return (
<div>
<button onClick={addCircle}>Add circle</button>
<ul>{elements.map(id => <li key={id}>{id}</li>)}</ul>
</div>
);
}
VectisDoc.create() and exchange individual ops as well as deltas — the op-level convenience surface (toArray(), applyDelta()) used by the SyncInk canvas. The core binary path is the delta() / apply() pair documented in the API reference.Capacity & default limits
The defaults are tuned for collaborative canvases in the browser:
16 KB
WASM binary — total runtime size
65k
objects per doc — max ordered items
256
max peers — concurrent collaborators
∞
operations — append-only log
© 2026 VectisCRDT contributors · MIT · Built with Rust → wasm32-unknown-unknown.