End-to-end encrypted real-time collaboration SDK. Self-hosted in one Docker command.
Add Google Docs-style multi-user editing, presence, and chat to any web app — where your server mathematically cannot read plaintext. Built on WebRTC + Yjs + AES-256-GCM via the browser's Web Crypto API. Client SDK + React hooks are MIT; signaling server is Apache 2.0.
- Zero-knowledge server — room keys live only in the browser. The server sees hashed identifiers and opaque ciphertext. Even under subpoena, there is nothing to disclose.
- Architecture supports regulated workflows — HIPAA technical safeguards, attorney-client privilege, GDPR data-minimization by design. (Not certified — architecture enables your own compliance program.)
- Self-hosted — run on your own Hetzner / AWS / bare metal via one Docker image. No vendor-cloud dependency.
- Fully open-source — Client SDK + React hooks are MIT, signaling server is Apache 2.0. Auditable, no proprietary crypto, no license fees for self-hosting.
- Real React hooks —
@tovsa7/zerosync-reactfor declarative integration (useYText,usePresence,useMyPresence…). Works with Tiptap, CodeMirror, Quill via standard Yjs bindings. - Encrypted-at-rest persistence (v0.2.0+) — opt-in IndexedDB store keyed by a domain-separated derivative of your
userSecret. Doc state survives reload beforeRoom.joinresolves; the on-disk row is ciphertext only. - Comprehensive test suite — property-based via
fast-check, integration tests, and headless-browser E2E (persistence reload, ciphertext-on-disk, IV randomness). OpenSSF Best Practices badge. SLSA provenance on every npm release.
ZeroSync is designed for products where two or more humans collaborate on sensitive content in real time, and where "your server cannot read it" is itself a feature:
- Legal tech — privileged attorney-client collaboration, live document redlines, e-signing ceremonies with witnesses
- Mental health / therapy — therapist-client sessions with notes, homework, and chat that the platform itself cannot see
- Finance / fintech — token deal rooms, M&A data rooms, OTC trading desk coordination, private equity deal flow
- Enterprise R&D — cross-team collaboration on IP, patents, regulatory filings, trade secrets
- Regulated SaaS with EU customers — a DPA-grade architecture you can point at during procurement
If your product is single-user, async-only, or entirely AI-driven — ZeroSync is probably not your fit.
npm install @tovsa7/zerosync-react @tovsa7/zerosync-client react yjsimport { ZeroSyncProvider, useYText, useConnectionStatus } from '@tovsa7/zerosync-react'
import { deriveRoomKey } from '@tovsa7/zerosync-client'
function App({ roomKey }: { roomKey: CryptoKey }) {
return (
<ZeroSyncProvider
serverUrl="wss://sync.example.com/ws"
roomId="my-room"
roomKey={roomKey}
peerId={crypto.randomUUID()}
nonce={btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16))))}
hmac=""
iceServers={[{ urls: 'stun:stun.l.google.com:19302' }]}
>
<Editor />
</ZeroSyncProvider>
)
}
function Editor() {
const status = useConnectionStatus()
const text = useYText('editor')
if (status !== 'connected') return <p>Status: {status}</p>
return <textarea value={text?.toString() ?? ''} onChange={(e) => {
text?.delete(0, text.length); text?.insert(0, e.target.value)
}} />
}See the React hooks package for useYMap, useYArray, usePresence, useMyPresence, and Tiptap / CodeMirror integration examples.
npm install @tovsa7/zerosync-client yjsimport { Room, deriveRoomKey } from '@tovsa7/zerosync-client'
// Room key is derived client-side and never transmitted.
const secret = crypto.getRandomValues(new Uint8Array(32))
const roomKey = await deriveRoomKey(secret, 'my-room-id')
const room = await Room.join({
serverUrl: 'wss://your-server/ws',
roomId: 'my-room-id',
roomKey,
peerId: crypto.randomUUID(),
nonce: btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16)))),
hmac: '',
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
})
const doc = room.getDoc()
const text = doc.getText('editor')
text.observe(() => console.log(text.toString()))
room.updatePresence({ name: 'Alice' })
room.leave() ╔═══════════════ AES-256-GCM ciphertext ═══════════════╗
▼ ▼
┌───────────┐ ┌───────────┐
│ Browser A │ ◄──────── WebRTC DataChannel (P2P) ────────►│ Browser B │
│ 🔑 │ │ 🔑 │
└─────┬─────┘ └─────┬─────┘
│ │
│ signaling only (ICE / SDP) │
└────────────────► ┌──────────────────┐ ◄────────────────┘
│ ZeroSync Server │
│ no user data │
└──────────────────┘
- P2P by default — peers connect directly via WebRTC DataChannel. User data never touches the server.
- Signaling-only server — exchanges ICE candidates / SDP between peers so they can find each other, then gets out of the way.
- Zero-knowledge server — holds no keys, logs only SHA-256-hashed room / peer IDs.
- Mutual peer auth — AES-GCM challenge-response on DataChannel open proves both peers possess the room key without transmitting it.
- Encrypted blob fallback — when strict NATs prevent direct P2P, the signaling server forwards opaque ciphertext blobs in-memory between currently connected peers. Server still cannot decrypt — it sees only ciphertext and hashed identifiers.
Full threat model + disclosure process: SECURITY.md · Regulatory mappings (HIPAA §164.312, GDPR Art. 25/32/33/34, SOC 2 CC6): COMPLIANCE.md · Security contact: .well-known/security.txt
| ZeroSync | Liveblocks | Yjs + y-websocket | Jazz.tools | |
|---|---|---|---|---|
| End-to-end encrypted | ✅ AES-256-GCM | ❌ cloud reads data | ❌ | |
| Self-hosted | ✅ one Docker | ❌ cloud only | ✅ | ✅ |
| Zero-knowledge server | ✅ | ❌ | ❌ | ❌ |
| Open-source client | MIT | Proprietary | MIT | MPL-2.0 |
| Open-source server | Apache 2.0 | — (proprietary cloud) | MIT | MPL-2.0 |
| CRDT sync | Yjs | Proprietary | Yjs | Custom CoJSON |
| React hooks | ✅ | ✅ | community | ✅ |
Run your own signaling server:
docker run -p 8080:8080 ghcr.io/tovsa7/zerosync-server:latestFor production (auto-TLS via Caddy, encrypted relay fallback for strict NATs), see the self-hosted guide.
Point the SDK at your server:
const room = await Room.join({
serverUrl: 'wss://sync.example.com/ws',
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
// ...
})Room.join(opts) → Promise<Room> — connects, joins the room, starts sync + presence.
| Option | Type | Description |
|---|---|---|
serverUrl |
string |
WebSocket URL of the signaling server |
roomId |
string |
Room identifier (opaque to the server) |
roomKey |
CryptoKey |
AES-256-GCM key — never transmitted |
peerId |
string |
UUIDv4 for this peer |
nonce |
string |
Base64 random bytes for replay protection |
hmac |
string |
HMAC-SHA-256 of the HELLO message |
iceServers |
RTCIceServer[] |
WebRTC ICE servers. Pass [] to disable STUN (same-network P2P only). |
persistence (optional) |
EncryptedPersistence |
Encrypted-at-rest IndexedDB store. State is loaded and applied before Room.join resolves; subsequent updates are saved on a 500 ms debounce. See client README. |
Room methods: getDoc() / updatePresence() / onPresence() / getPresence() / onStatus() / getConnectionSummary() / leave() — see packages/client/src/room.ts for full spec.
Helpers:
deriveRoomKey(secret, roomId)— HKDF-SHA-256,info="zerosync-room:{roomId}". Wire encryption key.derivePersistKey(secret, roomId)— HKDF-SHA-256,info="zerosync-persist:{roomId}". At-rest encryption key, domain-separated fromroomKey.EncryptedPersistence.open({ roomId, key })— per-room IDB store (zerosync-persistence-{roomId});load()/save()/clear()/close(). Caller owns lifecycle.
Declarative React hooks layered on the client SDK:
| Hook | Returns |
|---|---|
<ZeroSyncProvider> |
Context provider — calls Room.join on mount, leave on unmount; optional persistKey prop for at-rest persistence |
useRoom() |
Room | null |
useConnectionStatus() |
'connecting' | 'connected' | 'reconnecting' | 'closed' |
useYText(name) |
Y.Text | null (re-renders on update) |
useYMap(name) |
Y.Map | null (re-renders on update) |
useYArray(name) |
Y.Array | null (re-renders on update) |
usePresence<T>() |
ReadonlyMap<peerId, T> |
useMyPresence<T>() |
[T | null, setState] — broadcasts via room.updatePresence |
Re-exports derivePersistKey from the client SDK so React consumers don't need a direct dependency on @tovsa7/zerosync-client.
Full docs + Tiptap / CodeMirror / cursor-presence + persistence examples: packages/react/README.md.
| Property | Detail |
|---|---|
| Encryption | AES-256-GCM via Web Crypto API |
| IV | 12 random bytes per message — never reused |
| Key derivation | HKDF-SHA-256 |
| Domain separation | Wire roomKey and at-rest persistKey are independently derived from the same user secret (different HKDF info); compromise of one does not expose the other |
| At-rest encryption | Optional IndexedDB store, ciphertext only, per-room database |
| Server visibility | Hashed room/peer IDs and ICE candidates only |
| Peer auth | AES-GCM challenge-response handshake on DataChannel open |
| Relay blobs | Max 64 KB · opaque ciphertext · forwarded in-memory by the signaling server when P2P fails |
| Third-party crypto | None — crypto.subtle only |
The room key is derived client-side and never leaves the browser. Even under a court order, the server cannot provide document contents — it does not possess the keys.
Disclosure process + threat model: SECURITY.md.
Requires Web Crypto API, WebRTC DataChannel, and WebSocket. Chrome 89+, Firefox 78+, Safari 15+, Edge 89+. Node.js ≥ 20 for server-side integrations.
packages/client/ TypeScript SDK (@tovsa7/zerosync-client on npm)
packages/react/ React hooks package (@tovsa7/zerosync-react)
demo/ React collaborative editor demo
landing/ Astro landing page (deploys to github.io/ZeroSync)
The Apache 2.0 signaling server lives in github.com/tovsa7/zerosync-self-hosted. Architecture/protocol/compliance documentation is in this repo's SECURITY.md, COMPLIANCE.md, and SELF-HOSTED.md.
Running ZeroSync in production? I'm actively working with design partners building HIPAA/GDPR-sensitive collaboration apps. If you need:
- Direct support from the maintainer
- Priority on feature requests and roadmap input
- Help with self-hosted deployment
- Custom integrations or audit-readiness consultation
Email [email protected] with a short description of your use case.
A paid enterprise plugin (admin dashboard, SSO, audit log) is in development — reach out if you'd like to be notified when it ships, or to discuss design-partner terms with grandfathered pricing.
Client SDK + React hooks (@tovsa7/zerosync-client, @tovsa7/zerosync-react): MIT — see LICENSE.
Signaling server (ghcr.io/tovsa7/zerosync-server): Apache 2.0 — see the server repository.
Fully open-source. No production license fees. Forever.
Questions? Commercial / enterprise inquiries? [email protected]