Phase 1 — xSync core

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.

13 packagesisomorphicS3-backed
xsync-quickstart.ts
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.

FeaturexSyncZeroReplicache†ConvexTinyBaseHypercore/PearGun.jsYjsAutomergeLiveblocksPartyKit
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.

Client A

No messages yet.

Client B

No messages yet.

Event log0 events

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.

Next.js API
POST /events
🔐
Sign & hash
Ed25519 + SHA-256
📡
SSE / WS push
events.push →
🌐
Browser clients
IndexedDB + views
📦
S3WORM
immutable log
app/api/events/route.ts
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 })
}
components/live-feed.tsx
"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.

offline-firstmulti-writerBroadcastChannel
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.

BroadcastChannelIndexedDBoffline-first
// 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.

claimsS3WORMmulti-runtime
// 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.

S3WORMimmutablecompliance
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,
        },
      ],
    },
  },
})