Y.js Integration for Real-time Collaboration in Mobile Applications
Y.js — CRDT library in JavaScript increasingly pulled into React Native projects, expecting Google Docs experience in mobile. Reality more complex: Y.js designed for browser environment, no official Flutter SDK, and Hermes on old RN versions meets WASM binary @automerge/automerge with panic on init. Let's address real pitfalls.
Y.js Sync Mechanism
Each Y.Doc contains internal state-vector — Map<clientId, maxClock>. On two client connection they exchange state-vectors and request only delta: Y.encodeStateAsUpdateV2(doc, remoteStateVector). Differential protocol — on reconnect don't need to send entire document.
Transport level implemented via providers:
| Provider | Transport | Features |
|---|---|---|
y-websocket |
WebSocket | Official, has server part |
y-webrtc |
WebRTC DataChannel | P2P, not in RN without polyfill |
y-indexeddb |
IndexedDB | Browser only |
| Custom | SQLite / AsyncStorage | Manual RN implementation needed |
For React Native: y-websocket works on transport via react-native-get-random-values + native WebSocket. Persistence — custom provider over react-native-sqlite-storage or op-sqlite.
Persistence via SQLite in React Native
No ready y-sqlite provider for RN. Minimal implementation:
import * as Y from 'yjs';
import { openDatabase } from 'react-native-sqlite-storage';
const db = openDatabase({ name: 'collab.db' });
// Initialize table
db.transaction(tx => {
tx.executeSql(
'CREATE TABLE IF NOT EXISTS ydocs (id TEXT PRIMARY KEY, update BLOB, ts INTEGER)'
);
});
// Save on each change
ydoc.on('updateV2', (update: Uint8Array, origin: unknown) => {
if (origin === 'sqlite-load') return; // don't loop
const encoded = Buffer.from(update).toString('base64');
db.transaction(tx => {
tx.executeSql(
'INSERT OR REPLACE INTO ydocs (id, update, ts) VALUES (?, ?, ?)',
[docId, encoded, Date.now()]
);
});
});
// Load on opening
db.transaction(tx => {
tx.executeSql('SELECT update FROM ydocs WHERE id = ?', [docId], (_, result) => {
if (result.rows.length > 0) {
const raw = Buffer.from(result.rows.item(0).update, 'base64');
Y.applyUpdateV2(ydoc, new Uint8Array(raw), 'sqlite-load');
}
});
});
Problem with approach: on frequent edits (real-time typing) updateV2 triggers per character. Batching mandatory — debounce 300–500ms or accumulate via Y.mergeUpdatesV2. Without it production quickly runs out of device space, SQLite transactions block JS thread.
Server Part: y-websocket vs Custom Server
Official y-websocket server minimalist — stores documents in memory. For production need:
-
Persistence — save
Y.encodeStateAsUpdateV2()on last client disconnect. LevelDB (packagey-leveldb) or PostgreSQL with BYTEA column work. -
Authorization —
y-websocketdoesn't check tokens. Need middleware intercepting Upgrade request and checking JWT before upgrade. - Scaling — one y-websocket process doesn't know others. For horizontal scaling — Redis PubSub as bus between nodes.
Alternative: Hocuspocus (y-websocket wrapper with auth, hooks and ready persistence). For most projects Hocuspocus covers 90% server needs without custom code.
Common RN Integration Mistakes
clientID Y.js generated randomly on Y.Doc creation. Creating new Y.Doc per component mount — client gets new ID after each unmount, server state-vector accumulates dead records. Fix: store ydoc in ref or global state, don't recreate.
Awareness (cursors, online status) via y-protocols/awareness requires active WebSocket. On iOS app backgrounding, WebSocket may be killed in 30–60 seconds. Call awareness.setLocalState(null) in AppState.change → background handler, otherwise user hangs in online participants list after app background.
Flutter: Y.js via JS Runtime
No native Flutter port of Y.js. Options:
-
flutter_js— runs V8/QuickJS, weighs ~5 MB. Y.js works, but performance on large documents wanting. - Native Dart CRDT:
crdtpackage by Cachapa — implements LWW-CRDT, incompatible with Y.js protocol. - Rust FFI via
yrs(Rust Y.js implementation) +flutter_rust_bridge— most performant path, but 4–6 weeks just on bindings.
Assessment
React Native + Y.js + custom SQLite provider + Hocuspocus backend: 6–10 weeks. Flutter via yrs FFI: 10–16 weeks. Includes: persistence, awareness, reconnect logic with exponential backoff, tests for conflicts on concurrent editing. Cost calculated individually after requirements analysis.







