Skip to main content
@livestore/sync-cf is the official LiveStore sync backend built on Cloudflare Workers and Durable Objects. It enables real-time synchronization across multiple clients using WebSocket connections. The package has three entry points:
Entry pointPurpose
@livestore/sync-cf/cf-workerCloudflare Worker and Durable Object code (server side)
@livestore/sync-cf/clientSync client transports (browser/Node.js/Expo)
@livestore/sync-cf/commonShared types used by both sides

Installation

npm install @livestore/sync-cf

Client — @livestore/sync-cf/client

makeWsSync

Creates a WebSocket-based sync backend. Pass this to the adapter’s sync.backend option.
function makeWsSync(options: WsSyncOptions): SyncBackend.SyncBackendConstructor<SyncMetadata>

Parameters

options.url
string
required
URL of the sync backend. Accepts http/https or ws/wss protocols.
url: 'wss://my-worker.example.workers.dev'
options.ping.enabled
boolean
default:"true"
Enable WebSocket ping/pong keepalive.
options.ping.requestTimeout
Duration
default:"10 seconds"
How long to wait for a ping response before marking the connection as offline.
options.ping.requestInterval
Duration
default:"10 seconds"
How often to send ping requests.
options.webSocketFactory
(url: string) => Effect<WebSocket, WebSocketError, Scope>
Optional custom WebSocket factory for environments with non-standard WebSocket APIs.

Example

import { makeWsSync } from '@livestore/sync-cf/client'
import { makePersistedAdapter } from '@livestore/adapter-web'

const adapter = makePersistedAdapter({
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
  storage: { type: 'opfs' },
  sync: {
    backend: makeWsSync({ url: 'wss://my-worker.example.workers.dev' }),
  },
})

Server — @livestore/sync-cf/cf-worker

makeDurableObject

Creates a Cloudflare Durable Object class that handles sync. One Durable Object instance is created per storeId.
function makeDurableObject(options?: MakeDurableObjectClassOptions): {
  new (ctx: DurableObjectState, env: Env): DurableObject & SyncBackendRpcInterface
}

Options

options.onPush
(message: PushRequest, context: CallbackContext) => void | Promise<void>
Called when a client pushes events. Use this for server-side validation of individual push operations.
onPush: async (message, { storeId, payload, headers }) => {
  // validate message.batch
}
options.onPull
(message: PullRequest, context: CallbackContext) => void | Promise<void>
Called when a client requests to pull events.
options.forwardHeaders
string[] | ((request) => Record<string, string>)
Forward request headers into onPush/onPull callbacks. Useful for cookie-based or header-based authentication inside the Durable Object.
// Forward specific headers by name
forwardHeaders: ['cookie', 'authorization']

// Or extract custom values
forwardHeaders: (request) => ({
  'x-user-id': request.headers.get('x-user-id') ?? '',
})
options.storage
{ _tag: 'do-sqlite' } | { _tag: 'd1'; binding: string }
Storage engine for event persistence. Defaults to { _tag: 'do-sqlite' } (Durable Object SQLite).Use d1 when you want a centralized, externally queryable database at the cost of an extra network hop.
options.enabledTransports
Set<'http' | 'ws' | 'do-rpc'>
Which transport protocols to accept. Defaults to all three.
options.otel
{ baseUrl?: string; serviceName?: string }
OpenTelemetry configuration for the Durable Object.

Example Durable Object

worker.ts
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'

export class SyncBackendDO extends makeDurableObject({
  onPush: async (message, { storeId }) => {
    console.log(`Push to ${storeId}:`, message.batch.length, 'events')
  },
}) {}

makeWorker

Creates a Cloudflare Worker fetch handler that routes sync requests to the Durable Object. For simpler setups you can use this instead of writing a custom fetch.
function makeWorker<TEnv, TDurableObjectRpc, TSyncPayload>(
  options: MakeWorkerOptions<TEnv, TSyncPayload>,
): CFWorker<TEnv, TDurableObjectRpc>

Options

options.syncBackendBinding
string
required
The Durable Object binding name declared in wrangler.toml.
options.validatePayload
(payload, context) => void | Promise<void>
Validates the client-provided payload during WebSocket connection. Throw to reject the connection.The context includes storeId and headers (the raw request headers, regardless of forwardHeaders on the DO).
validatePayload: async (payload, { storeId, headers }) => {
  const cookie = headers.get('cookie')
  const session = await validateSessionFromCookie(cookie)
  if (!session) throw new Error('Unauthorized')
}
options.syncPayloadSchema
Schema.Schema<TSyncPayload>
Optional Effect Schema to decode the client payload before passing it to validatePayload.
options.enableCORS
boolean
default:"false"
Add CORS headers to responses.

Example Worker

worker.ts
import { makeDurableObject, makeWorker } from '@livestore/sync-cf/cf-worker'

export class SyncBackendDO extends makeDurableObject() {}

export default makeWorker({
  syncBackendBinding: 'SYNC_BACKEND_DO',
  enableCORS: true,
})

handleSyncRequest

Lower-level alternative to makeWorker. Handles a single LiveStore sync request. Use this when you have a custom fetch handler and want to forward only matching requests to LiveStore.
function handleSyncRequest(options: { ... }): Promise<Response>

Wrangler configuration

wrangler.toml
name = "my-sync-worker"
main = "src/worker.ts"
compatibility_date = "2024-11-01"

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]
Use new_sqlite_classes (not new_classes) so the Durable Object gets the Durable Object SQLite storage backend.

Full setup example

1

Define your schema

schema.ts
import { Events, makeSchema, Schema, State } from '@livestore/livestore'

export const events = {
  todoCreated: Events.synced({
    name: 'v1.TodoCreated',
    schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
  }),
}

export const schema = makeSchema({
  events,
  state: State.SQLite.makeState({ tables: { /* ... */ } }),
})
2

Create the Cloudflare Worker

src/worker.ts
import { makeDurableObject, makeWorker } from '@livestore/sync-cf/cf-worker'

export class SyncBackendDO extends makeDurableObject() {}

export default makeWorker({
  syncBackendBinding: 'SYNC_BACKEND_DO',
})
3

Configure wrangler.toml

wrangler.toml
name = "my-sync-worker"
main = "src/worker.ts"
compatibility_date = "2024-11-01"

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]
4

Connect from the client

app.ts
import { makeWsSync } from '@livestore/sync-cf/client'
import { makePersistedAdapter } from '@livestore/adapter-web'

const adapter = makePersistedAdapter({
  worker: LiveStoreWorker,
  sharedWorker: LiveStoreSharedWorker,
  storage: { type: 'opfs' },
  sync: {
    backend: makeWsSync({ url: 'wss://my-sync-worker.example.workers.dev' }),
  },
})

Peer dependencies

PackageVersion
effect^3.19.19
@cloudflare/workers-types4.x

Build docs developers (and LLMs) love