loro-protocol is a small, transport-agnostic syncing protocol for collaborative CRDT documents. This repo hosts the protocol implementation, a WebSocket client, and minimal servers for local testing or self‑hosting.
- Protocol: multiplex multiple rooms on one connection, 256 KiB max per message, large update fragmentation supported
- CRDTs: Loro document, Loro ephemeral store; extensible (e.g., Yjs, Yjs Awareness)
- Transports: WebSocket or any integrity-preserving transport (e.g., WebRTC)
See protocol.md for the full wire spec.
packages/loro-protocol(MIT): Core TypeScript definitions, encoders/decoders for the wire protocolpackages/loro-websocket(MIT): WebSocket client + a SimpleServer for local testingpackages/loro-adaptors(MIT): Shared CRDT adaptors for Loro documents and ephemeral state
Rust workspace (AGPL):
rust/loro-protocol: Rust encoder/decoder mirroring the TS implementationrust/loro-websocket-client: Async WS client for the protocolrust/loro-websocket-server: Minimal async WS server with optional SQLite snapshotting
Use the minimal WebSocket server for local development and tests.
- Install dependencies
pnpm install
pnpm -r build- Start a SimpleServer (Node.js)
pnpm dev-simple-server- Connect a client and sync a Loro document
// examples/client.ts
import { LoroWebsocketClient } from "loro-websocket";
import { LoroAdaptor } from "loro-adaptors";
// In Node, provide a WebSocket implementation
import { WebSocket } from "ws";
(globalThis as any).WebSocket = WebSocket;
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
await client.waitConnected();
const adaptor = new LoroAdaptor();
const room = await client.join({ roomId: "demo-room", crdtAdaptor: adaptor });
// Edit the shared doc
const text = adaptor.getDoc().getText("content");
text.insert(0, "Hello, Loro!");
adaptor.getDoc().commit();
// Later…
await room.destroy();Tip: For a working reference, see packages/loro-websocket/src/e2e.test.ts which spins up SimpleServer and syncs two clients end‑to‑end.
%ELO adds end‑to‑end encryption to Loro sync. The server never decrypts; it indexes plaintext headers only to support backfill and routing. Clients encrypt/decrypt using AES‑GCM with a 12‑byte IV and the exact encoded header bytes as AAD.
- TypeScript: use
EloLoroAdaptorfromloro-adaptors+LoroWebsocketClient.- Provide a
getPrivateKey()hook that resolves{ keyId, key }(Web Crypto CryptoKey or Uint8Array). - The adaptor packages updates into
%ELOcontainers and decrypts incoming ones, applying to its internalLoroDoc.
- Provide a
Example (Node 18+):
import { LoroWebsocketClient } from "loro-websocket/client";
import { EloLoroAdaptor } from "loro-adaptors";
import { WebSocket } from "ws";
(globalThis as any).WebSocket =
WebSocket as unknown as typeof globalThis.WebSocket;
const key = new Uint8Array([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
]);
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
await client.waitConnected();
const adaptor = new EloLoroAdaptor({
getPrivateKey: async () => ({ keyId: "k1", key }),
});
const room = await client.join({ roomId: "elo-room", crdtAdaptor: adaptor });
// Edit the encrypted doc
const text = adaptor.getDoc().getText("t");
text.insert(0, "hello");
adaptor.getDoc().commit();
await room.destroy();Notes:
- Use a unique, non‑repeating 12‑byte IV per encryption for security (the adaptor accepts an optional
ivFactory()for testing); the examples may fix IVs for determinism in tests only. - Keys and key agreement are application‑provided and out of scope.
We provide cross‑language tests to verify %ELO interoperability between the TS and Rust implementations.
- Run with pnpm:
pnpm run test:cross-langThis will:
- Run the Rust cross‑lang e2e test (
rust/loro-websocket-server/tests/elo_cross_lang.rs) with logs. - Spawn thin TS test-wrappers via
pnpm exec tsx:packages/loro-websocket/test-wrappers/start-simple-server.tspackages/loro-websocket/test-wrappers/send-elo-normative.tspackages/loro-websocket/test-wrappers/recv-elo-doc.ts
- Use the Rust example
rust/loro-websocket-client/examples/elo_index_client.rswhich encrypts/decrypts real%ELOcontainers.
Requirements:
- Node 18+, pnpm (or npx fallback), Rust toolchain.
- For CI stability, you can run with a single test thread:
cargo test -p loro-websocket-server --test elo_cross_lang -- --ignored --nocapture --test-threads=1.
SimpleServer accepts optional hooks for basic auth and persistence:
const server = new SimpleServer({
port: 8787,
authenticate: async (roomId, crdt, auth) => {
// return 'read' | 'write' | null to deny
return "write";
},
onLoadDocument: async (roomId, crdt) => null, // return snapshot bytes
onSaveDocument: async (roomId, crdt, data) => {
// persist snapshot bytes somewhere (e.g., filesystem/db)
},
saveInterval: 60_000, // ms
});The Rust workspace contains a minimal async WebSocket server (loro-websocket-server) with optional SQLite persistence. See rust/loro-websocket-server/examples/simple-server.rs for a CLI example.
- Magic bytes per CRDT: "%LOR" (Loro doc), "%EPH" (Loro ephemeral), "%YJS", "%YAW", …
- Messages: JoinRequest/JoinResponseOk/JoinError, DocUpdate, DocUpdateFragmentHeader/Fragment, UpdateError, Leave
- Limits: 256 KiB per message; large updates must be fragmented; default reassembly timeout 10s
- Multi‑room: room ID is part of every message; one connection can join multiple rooms
See protocol.md for the full description and error codes.
- Build all:
pnpm -r build - Test all:
pnpm -r test - Cross‑lang E2EE test:
pnpm run test:cross-lang - Typecheck:
pnpm -r typecheck - Lint:
pnpm -r lint
Node 18+ is required for local development.
loro-protocol: MITloro-websocket: MITloro-adaptors: MIT- Rust workspace crates under
rust/: MIT
.
├── protocol.md # Wire protocol spec
├── packages/
│ ├── loro-protocol/ # Core encoders/decoders (MIT)
│ ├── loro-websocket/ # Client + SimpleServer (MIT)
│ ├── loro-adaptors/ # Shared CRDT adaptors (MIT)
├── rust/ # Rust implementations (AGPL)
│ ├── loro-protocol/
│ ├── loro-websocket-client/
│ └── loro-websocket-server/
└── pnpm-workspace.yaml
- How do I test locally? Use
SimpleServerinloro-websocketor the Rust server. - Can I bring my own auth/storage? Yes —
SimpleServerand the Rust server provide hooks for auth and persistence.