Signed actor logs.
Replicated everywhere.
xSync gives you a typed JSON bucket that stays in sync. Every write is Ed25519-signed, content-hashed, and causally ordered — across browser tabs, web workers, Next.js APIs, Node, and S3.
import { createXSync } from "@xsync/client"
import { broadcastChannelTransport } from "@xsync/transport-broadcast-channel"
type Message = { id: string; text: string; author: string }
const sync = await createXSync({
views: {
messages: {
initial: [] as Message[],
reduce: (msgs, event) =>
event.type === "chat.message"
? [...msgs, event.payload as Message]
: msgs,
},
},
transports: [broadcastChannelTransport("my-app")],
})
// Write — ed25519-signed, sha256-hashed, causal
await sync.emit("chat.message", {
id: crypto.randomUUID(),
text: "Hello from any runtime!",
author: sync.identity.id,
})
// Read — plain JS object, always fresh
console.log(sync.state.messages)
// [{ id: "...", text: "Hello from any runtime!", author: "peer_..." }]
// React
const { messages } = useXSync(sync)Why xSync
Built for distributed reality
Not a hosted service. Not another CRDT library. A sync fabric that runs where your code runs, with cryptographic guarantees at every layer.
Cryptographically signed
Every event carries an Ed25519 signature and SHA-256 content hash. No peer can forge or tamper with events — invalid signatures are silently dropped.
Truly isomorphic
Same API, same event format in browser tabs, web workers, Next.js API routes, Node.js workers, Cloudflare Workers, or Fly.io instances.
S3-backed durability
Events persist to S3-compatible storage as immutable WORM objects. Nodes sync from S3 after going offline — even if every peer was down simultaneously.
Bring your own everything
Swap transports (WebSocket, SSE, BroadcastChannel, HTTP polling), stores (IndexedDB, filesystem, S3), and views without changing your application code.
JSON bucket API
Read state like a plain object: sync.state.messages. Write with sync.emit(). Subscribe with sync.subscribe(). Plug into React with useXSync(sync).
Claim semantics
Built-in work queue views with lease, deterministic-election, authority, and optimistic-idempotent claim strategies. Distributed task coordination without a coordinator.
Multi-writer Merkle DAG
Events from multiple writers are merged into a causal DAG. Views reduce the full DAG deterministically — the same events always produce the same state.
Actor model
Every node, peer, workflow, step, and user is an actor with its own identity and event log. Actors join topics, emit events, and materialize views independently.
Comparison
How it stacks up
xSync is the only open-source solution combining S3-backed persistence, signed events, a BYO transport/store model, and built-in claim semantics.
| Feature | xSync | Zero | Replicache† | Convex | TinyBase | Hypercore/Pear | Gun.js | Yjs | Automerge | Liveblocks | PartyKit |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Self-hostable | ✓ | ✓ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | — | — |
| S3 / durable persistence | ✓ | — | — | — | — | — | — | — | — | — | — |
| Signed events (Ed25519) | ✓ | — | — | — | — | ✓ | ~ | — | — | — | — |
| TypeScript-first | ✓ | ✓ | ✓ | ✓ | ✓ | — | — | ~ | ~ | ✓ | ✓ |
| Offline-first | ✓ | ~ | ✓ | — | ✓ | ✓ | ✓ | ✓ | ✓ | — | — |
| BYO transport | ✓ | — | ~ | — | ~ | ✓ | ~ | ~ | ~ | — | — |
| BYO store / backend | ✓ | ~ | ~ | — | ✓ | ✓ | ~ | ~ | ~ | — | — |
| Actor / identity model | ✓ | — | — | ~ | — | ✓ | ~ | — | — | — | — |
| Work queue / claims | ✓ | — | — | ~ | — | — | — | — | — | ~ | ~ |
| Multi-writer causal DAG | ✓ | — | — | — | — | ✓ | ~ | ✓ | ✓ | — | — |
| P2P / serverless sync | ✓ | — | — | — | — | ✓ | ✓ | ~ | ~ | — | — |
| Open source | ✓ | ✓ | ✓ | ~ | ✓ | ✓ | ✓ | ✓ | ✓ | — | — |
| Isomorphic (browser + Node) | ✓ | — | — | — | ✓ | ~ | ✓ | ✓ | ✓ | — | ~ |
| Actively maintained | ✓ | ✓ | — | ✓ | ✓ | ~ | ~ | ✓ | ✓ | ✓ | ✓ |
~ = partial support / community plugins / workarounds required · † = maintenance mode
Live demo
P2P sync in your browser
Two independent xSync clients, connected only via BroadcastChannel. Type in either panel — signed events replicate instantly. The event log shows real hashes and signatures.
No messages yet.
No messages yet.
Events will appear here…
All events are signed with Ed25519 — open DevTools and inspect the network to see zero server round-trips
Architecture
Server writes. Clients receive. S3 remembers.
A Next.js API route emits a signed event. All connected browser clients receive it instantly via SSE. IndexedDB stores it locally. S3WORM archives it forever.
import { createXSync } from "@xsync/client"
import { s3wormStore } from "@xsync/store-s3worm"
import { sseTransport } from "@xsync/transport-sse"
// Server-side client — persists to S3, pushes via SSE
const server = await createXSync({
stores: [
s3wormStore({ bucket: "my-bucket", prefix: "xsync" }),
],
transports: [sseTransport("/api/xsync/stream")],
})
export async function POST(req: Request) {
const { type, payload } = await req.json()
// Signed, hashed, replicated
const event = await server.emit(type, payload)
return Response.json({ eventId: event.id })
}"use client"
import { createXSync } from "@xsync/client"
import { indexedDbStore } from "@xsync/store-indexeddb"
import { sseTransport } from "@xsync/transport-sse"
// Browser client — persists to IndexedDB, syncs via SSE
const client = await createXSync({
store: indexedDbStore("my-app"),
transports: [sseTransport("/api/xsync/stream")],
views: {
feed: {
initial: [] as FeedItem[],
reduce: (items, event) =>
event.type === "feed.item"
? [...items, event.payload as FeedItem]
: items,
},
},
})
// State is always fresh — no polling, no refetch
const { feed } = useXSync(client)Use cases
What you can build
xSync handles the sync layer so your application code stays pure. Every use case below runs with zero changes to the core — just swap transports and stores.
Collaborative whiteboards
Each cursor move, shape draw, and annotation is a signed event. Conflicts resolve deterministically via causal ordering. Works offline — syncs when reconnected.
const board = await createXSync({
views: {
shapes: {
initial: {} as Record<string, Shape>,
reduce: (shapes, event) => {
if (event.type === "shape.moved")
return { ...shapes, [event.payload.id]: event.payload }
if (event.type === "shape.deleted") {
const next = { ...shapes }
delete next[event.payload.id]
return next
}
return shapes
},
},
},
transports: [broadcastChannelTransport("whiteboard")],
})Cross-tab state sync
User opens your app in two tabs. Both tabs share the same actor log via BroadcastChannel — state is always consistent without a server round-trip.
// Tab A and Tab B share this client
const sync = await createXSync({
store: indexedDbStore("my-app"),
transports: [broadcastChannelTransport("my-app")],
views: {
cart: {
initial: { items: [] as CartItem[], total: 0 },
reduce: cartReducer,
},
},
})
// Adding to cart in Tab A → instantly in Tab B
await sync.emit("cart.item.added", { sku: "lamp-001", qty: 1 })Distributed job queues
Workers across browser tabs, Next.js APIs, and Docker containers compete for jobs using xSync's built-in claim strategies — no Redis, no SQS, no coordinator.
// Enqueue work
await sync.emit("work.enqueued", {
workId: crypto.randomUUID(),
type: "video.transcode",
claimStrategy: { mode: "lease", ttlMs: 30_000, heartbeatMs: 5_000 },
})
// Worker claims it
const readyWork = sync.get("workQueue")
for (const item of Object.values(readyWork)) {
if (item.status === "ready") {
await sync.emit("claim.requested", {
claimId: crypto.randomUUID(),
workId: item.id,
claimantActorId: identity.id,
strategy: item.claimStrategy,
})
}
}Durable audit logs
Every mutation is a signed, immutable event written to S3WORM. No event can be deleted or tampered — perfect for compliance, audit trails, and debugging.
const audit = await createXSync({
store: s3wormStore({
bucket: "my-audit-bucket",
prefix: "audit/2026",
}),
views: {
// Materialise audit log from immutable events
log: {
initial: [] as AuditEntry[],
reduce: (entries, event) => [
...entries,
{
id: event.id,
actor: event.writer.publicKey,
type: event.type,
at: event.causality.timestamp,
hash: event.content.hash,
},
],
},
},
})