If you spin up an authoritative WebSocket server for a collaborative spreadsheet, two problems hit you immediately:
- The Infrastructure Tax: You are now paying for idle socket connections, Redis pub/sub adapters, and persistent database sync pipelines for users who might just leave a tab open in the background.
- The Data Liability: Translation catalogs regularly contain confidential unreleased feature keys, internal system copy, and proprietary strings. The moment those strings transit an unencrypted central server, you inherit compliance and breach blast-radius liabilities.
For JSON Link, I wanted zero backend hosting costs and absolute data sovereignty.
Here is how the browser-native peer-to-peer collaboration engine is engineered using Yjs CRDTs, WebRTC DataChannels, and Web Crypto (AES-GCM-256).
The State Topology: Serverless Mesh
Traditional collaborative apps route every mutation through a central coordinator:
Browser A ──[WebSocket]──> [Central Node + Redis/DB] ──[WebSocket]──> Browser B
Instead of managing server state, JSON Link runs a direct peer-to-peer data mesh:
Browser A <══════ WebRTC DataChannel (Direct P2P) ══════> Browser B
│ │
└─── [Ephemeral Signaling Nodes] ───┘
- Signaling is strictly for discovery: Public STUN and WebSocket signaling servers only broker the initial SDP offer/answer handshake and ICE candidates. No document payload is ever stored on signaling nodes.
- Direct browser transport: Once ICE negotiation completes, peers stream raw delta updates over SCTP-based WebRTC DataChannels. Latency drops to direct network ping (<20ms on LAN/regional connections).
- Deterministic CRDT convergence: Concurrent keystrokes on the same table cell resolve automatically using Yjs state vectors and Lamport timestamps. There is no "last-write-wins" database overwrite.
Zero-Knowledge Signaling: Client-Side PBKDF2 + AES-GCM
Because public WebRTC signaling nodes can inspect transit packets during connection setup, room updates must be encrypted before leaving the browser.
When a room password or PIN is configured, the browser derives an AES-GCM 256-bit encryption key using PBKDF2-SHA256 with 100,000 iterations:
export async function deriveRoomKey(password: string, roomSalt: string): Promise<CryptoKey> {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(password),
{ name: 'PBKDF2' },
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: enc.encode(roomSalt),
iterations: 100000,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
Every Yjs document delta is encrypted client-side with a unique 12-byte initialization vector (IV) before transmission. Even if an intermediary intercepts the signaling room traffic, all payloads remain opaque ciphertext.
Integrating Yjs into a Reactive Spreadsheet Grid
Binding CRDT updates to a React spreadsheet without triggering render thrashing requires separating the document state from ephemeral collaboration presence:
import * as Y from 'yjs';
import { WebrtcProvider } from 'y-webrtc';
const ydoc = new Y.Doc();
const yTranslations = ydoc.getMap('translations');
// Establish encrypted P2P mesh across fallback signaling clusters
const provider = new WebrtcProvider('json-link-room-id', ydoc, {
signaling: [
'wss://y-webrtc.fly.dev',
'wss://y-webrtc-signaling.fly.dev'
],
password: roomPin, // Native PBKDF2-SHA256 derivation under the hood
});
// 1. Synchronize CRDT changes into the table model
yTranslations.observe(() => {
setGridData(yTranslations.toJSON());
});
// 2. Ephemeral awareness (multiplayer cursors & focused cells)
provider.awareness.setLocalStateField('user', {
name: 'Alex-42',
color: '#10b981',
pointer: { x: clientX, y: clientY },
activeCell: { key: 'auth.login.title', field: 'en' }
});
Where P2P Sucks (And How to Mitigate It)
Full-mesh WebRTC is not magic; it comes with real-world engineering constraints:
- The $O(N^2)$ Bandwidth Limit: In a full mesh, every peer connects to every other peer. For 3–6 collaborators working on a localization file, overhead is negligible (a few KB/s). Beyond 10–12 peers, upstream bandwidth multiplies rapidly. For JSON Link's target use-case (small product pods localizing software), full mesh is the sweet spot.
-
NAT / Corporate Firewall Traversal:
Strict symmetric NATs block direct peer-to-peer hole-punching. We bundle public STUN endpoints (
stun:stun.l.google.com:19302) for standard NAT mapping, with self-hostable signaling flags for teams behind strict corporate firewalls. -
Offline Resilience & AST Protection:
When a peer disconnects, their edits persist locally via the File System Access API (direct two-way disk synchronization with
locales/*.json). On reconnect, Yjs automatically replays missing updates. To prevent non-technical contributors from accidentally breaking translation variables, an AST tokenizer locks ICU MessageFormat ({count, plural, ...}), Mustache, and Printf placeholders into immutable chips during live editing.
Code & Architecture
JSON Link is MIT licensed and runs 100% in the browser with no backend dependencies:
- Live App: https://json-link.pages.dev
- Source Code: https://github.com/pyaephyomaungdev/json-link
- Test Suite: 463 automated tests across 60 test suites (Vitest + React Testing Library)
Top comments (0)