‹ back to the set

vectis-crdt

rust · wasm · rga/yata · vector clocks

Conflict-free collaboration, shipped as 16 KB of WebAssembly. An RGA/YATA ordered sequence with per-property LWW registers — deterministic convergence, binary wire format, delta sync via vector clocks — built for collaborative vector canvases and rich ordered collections.

source ↗ live in syncink ↗

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:

  1. EDIT — produce an operation locally. insert(pos, obj) → RGA/YATA: a local op, no coordination with other peers.
  2. SYNC — exchange deltas with peers. doc.delta() → binary bytes: only the changed ops travel, keeping the wire minimal.
  3. 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:

runtime topology · 7 nodes, 6 edges, O(1) convergence
Operation → RGA/YATA → LWW Reg → Vector Clock → Delta Sync → Merge Engine → State
#NodeRoleDetail
01Operationinsert · deleteLocal op produced without coordination
02RGA/YATAsequence CRDTReplicated Growable Array, YATA variant
03LWW Regcolor · width · opacityLast-Write-Wins per-property register
04Vector ClockcausalityTracks causal ordering across peers
05Delta Syncbinary diffBinary-encoded minimal changeset
06Merge EngineconvergenceDeterministic CRDT merge algorithm
07Stateconsistent viewFinal consistent document state

The four merge stages

Four stages, one pass — each with a bounded cost:

StageStructureCostWhat it does
01 · RGA/YATAordered sequenceO(1)Ordered sequence insert. Concurrent inserts at the same position converge deterministically without coordination.
02 · LWW Registerper-property registerO(1)Last-write-wins properties. Concurrent writes to the same property are resolved by timestamp.
03 · Vector Clockper-peer countersO(peers)Causal ordering. Each peer's clock entry tracks what it has seen, so causally-related ops apply in order.
04 · Delta Mergebinary patchO(Δ)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.

performanceThe engine's memory is dominated by the append-only RGA log (~16 KB budget), with a smaller LWW table (~4 KB), the vector clock, and a delta buffer. The operation log is append-only, so operations are unbounded.

Install

vectis-crdt ships as an npm package wrapping the WebAssembly build, and as a Rust crate.

install · shell
npm install vectis-crdt
# or
pnpm add vectis-crdt
yarn add vectis-crdt
rust · cargo
cargo add vectis-crdt
noteThe package is built with Rust targeting 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 · typescript
import { 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());
tipVectisDoc.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

MethodDescriptionExample
doc.insert(pos, obj)Insert object at positiondoc.insert(0, { color: "#f00" })
doc.delete(id)Remove object by IDdoc.delete("obj-42")
doc.setProp(id, key, val)Set LWW propertydoc.setProp("obj-1", "color", "#00f")
doc.delta()Export changes as binaryconst bytes = doc.delta()
doc.apply(delta)Merge remote deltadoc.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.

noteThe operation log is append-only. Deletes are tombstoned rather than erased, which is what makes merging a delete against a concurrent edit conflict-free.

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 · javascript
import { 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 · javascript
import { 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 · react
import { 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>
  );
}
noteThe guide examples pass a peer id to 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.