Skip to content

Cloudflare Workers

The @livestore/sync-cf package provides a comprehensive LiveStore sync provider for Cloudflare Workers. It uses Durable Objects for connectivity and, by default, persists events in the Durable Object’s own SQLite. You can optionally use Cloudflare D1 instead. Multiple transports are supported to fit different deployment scenarios.

LiveStore ClientCloudflareWorkerDurable Object(per storeId)DO SQLite (default)or D1 (optional) Route bystoreId Read/Write WebSocket / HTTPpush & pull

Key responsibilities:

  • Worker: Routes sync requests to Durable Objects by storeId, handles auth validation
  • Durable Object: Manages sync state, handles push/pull operations, maintains WebSocket connections
  • Storage: Persists events in DO SQLite (default) or D1 (optional)
Terminal window
pnpm add @livestore/sync-cf

The sync provider supports three transport protocols, each optimized for different use cases:

Real-time bidirectional communication with automatic reconnection and live pull support.

import {
const makeWsSync: (options: WsSyncOptions) => SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
} from '@livestore/sync-cf/client'
export const
const syncBackend: SyncBackendConstructor<Struct.ReadonlySide<{
readonly _tag: tag<"SyncMessage.SyncMetadata">;
readonly createdAt: String;
}, "Type">, Json>
syncBackend
=
function makeWsSync(options: WsSyncOptions): SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
({
WsSyncOptions.url: string

URL of the sync backend

The protocol can either http/https or ws/wss

url
: 'wss://sync.example.com',
})

HTTP-based sync with polling for live updates. Requires the enable_request_signal compatibility flag.

import {
const makeHttpSync: (options: HttpSyncOptions) => SyncBackendConstructor<SyncMetadata>

Note: This implementation requires the enable_request_signal compatibility flag to properly support pull streaming responses

makeHttpSync
} from '@livestore/sync-cf/client'
export const
const syncBackend: SyncBackendConstructor<Struct.ReadonlySide<{
readonly _tag: tag<"SyncMessage.SyncMetadata">;
readonly createdAt: String;
}, "Type">, Json>
syncBackend
=
function makeHttpSync(options: HttpSyncOptions): SyncBackendConstructor<SyncMetadata>

Note: This implementation requires the enable_request_signal compatibility flag to properly support pull streaming responses

makeHttpSync
({
HttpSyncOptions.url: string

URL of the sync backend

@example

const syncBackend = makeHttpSync({ url: 'https://sync.example.com' })

url
: 'https://sync.example.com',
HttpSyncOptions.livePull?: {
pollInterval?: Input;
}
livePull
: {
pollInterval?: Input

How often to poll for new events

@default5 seconds

pollInterval
: 3000, // Poll every 3 seconds
},
})

Direct RPC communication between Durable Objects (internal use by @livestore/adapter-cloudflare).

import type {
import CfTypes
CfTypes
,
(alias) interface SyncBackendRpcInterface
import SyncBackendRpcInterface

Durable Object interface supporting the DO RPC protocol for DO <> DO syncing.

SyncBackendRpcInterface
} from '@livestore/sync-cf/cf-worker'
import {
const makeDoRpcSync: ({ syncBackendStub, durableObjectState, durableObjectContext, }: DoRpcSyncOptions) => SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses Durable Object RPC to communicate with the sync backend.

Used internally by @livestore/adapter-cf to connect to the sync backend.

makeDoRpcSync
} from '@livestore/sync-cf/client'
declare const
const state: CfTypes.DurableObjectState<unknown>
state
:
import CfTypes
CfTypes
.
interface DurableObjectState<Props = unknown>
DurableObjectState
declare const
const syncBackendDurableObject: CfTypes.DurableObjectStub<SyncBackendRpcInterface>
syncBackendDurableObject
:
import CfTypes
CfTypes
.
type DurableObjectStub<T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = (T extends CfTypes.Rpc.EntrypointBranded ? CfTypes.Rpc.Provider<T, "alarm" | "webSocketMessage" | "webSocketClose" | "webSocketError" | "fetch" | "connect"> : unknown) & {
fetch(input: CfTypes.RequestInfo | CfTypes.URL, init?: CfTypes.RequestInit): Promise<CfTypes.Response>;
connect(address: CfTypes.SocketAddress | string, options?: CfTypes.SocketOptions): CfTypes.Socket;
} & {
...;
}
DurableObjectStub
<
(alias) interface SyncBackendRpcInterface
import SyncBackendRpcInterface

Durable Object interface supporting the DO RPC protocol for DO <> DO syncing.

SyncBackendRpcInterface
>
export const
const syncBackend: SyncBackendConstructor<Struct.ReadonlySide<{
readonly _tag: tag<"SyncMessage.SyncMetadata">;
readonly createdAt: String;
}, "Type">, Json>
syncBackend
=
function makeDoRpcSync({ syncBackendStub, durableObjectState, durableObjectContext, }: DoRpcSyncOptions): SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses Durable Object RPC to communicate with the sync backend.

Used internally by @livestore/adapter-cf to connect to the sync backend.

makeDoRpcSync
({
DoRpcSyncOptions.syncBackendStub: SyncBackendRpcStub

Durable Object stub that implements the SyncDoRpc interface

syncBackendStub
:
const syncBackendDurableObject: CfTypes.DurableObjectStub<SyncBackendRpcInterface>
syncBackendDurableObject
,
DoRpcSyncOptions.durableObjectState: CfTypes.DurableObjectState<unknown>

State handle of the client DurableObject running this sync backend. Scopes live-pull routing to this instance so it resets when the DO is reconstructed (see

handleSyncUpdateRpc

).

durableObjectState
:
const state: CfTypes.DurableObjectState<unknown>
state
,
DoRpcSyncOptions.durableObjectContext: {
bindingName: string;
durableObjectId: string;
}

Information about this DurableObject instance so the Sync DO instance can call back to this instance

durableObjectContext
: {
bindingName: string

See wrangler.toml for the binding name

bindingName
: 'CLIENT_DO',
durableObjectId: string

state.id.toString() in the DO

durableObjectId
:
const state: CfTypes.DurableObjectState<unknown>
state
.
DurableObjectState<unknown>.id: CfTypes.DurableObjectId
id
.
DurableObjectId.toString(): string
toString
(),
},
})

Creates a WebSocket-based sync backend client.

Options:

  • url - WebSocket URL (supports ws/wss or http/https protocols)
  • webSocketFactory? - Custom WebSocket implementation
  • ping? - Ping configuration:
    • enabled?: boolean - Enable/disable ping (default: true)
    • requestTimeout?: Duration - Ping timeout (default: 10 seconds)
    • requestInterval?: Duration - Ping interval (default: 10 seconds)

Features:

  • Real-time live pull
  • Automatic reconnection
  • Connection status tracking
  • Ping/pong keep-alive
import {
const makeWsSync: (options: WsSyncOptions) => SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
} from '@livestore/sync-cf/client'
export const
const syncBackend: SyncBackendConstructor<Struct.ReadonlySide<{
readonly _tag: tag<"SyncMessage.SyncMetadata">;
readonly createdAt: String;
}, "Type">, Json>
syncBackend
=
function makeWsSync(options: WsSyncOptions): SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
({
WsSyncOptions.url: string

URL of the sync backend

The protocol can either http/https or ws/wss

url
: 'wss://sync.example.com',
WsSyncOptions.ping?: {
enabled?: boolean;
requestTimeout?: Input;
requestInterval?: Input;
}
ping
: {
enabled?: boolean

@defaulttrue

enabled
: true,
requestTimeout?: Input

How long to wait for a ping response before timing out

@default10 seconds

requestTimeout
: 5000,
requestInterval?: Input

How often to send ping requests

@default10 seconds

requestInterval
: 15000,
},
})

Creates an HTTP-based sync backend client with polling for live updates.

Options:

  • url - HTTP endpoint URL
  • headers? - Additional HTTP headers
  • livePull? - Live pull configuration:
    • pollInterval?: Duration - Polling interval (default: 5 seconds)
  • ping? - Ping configuration (same as WebSocket)

Features:

  • HTTP request/response based
  • Polling-based live pull
  • Custom headers support
  • Connection status via ping
import {
const makeHttpSync: (options: HttpSyncOptions) => SyncBackendConstructor<SyncMetadata>

Note: This implementation requires the enable_request_signal compatibility flag to properly support pull streaming responses

makeHttpSync
} from '@livestore/sync-cf/client'
export const
const syncBackend: SyncBackendConstructor<Struct.ReadonlySide<{
readonly _tag: tag<"SyncMessage.SyncMetadata">;
readonly createdAt: String;
}, "Type">, Json>
syncBackend
=
function makeHttpSync(options: HttpSyncOptions): SyncBackendConstructor<SyncMetadata>

Note: This implementation requires the enable_request_signal compatibility flag to properly support pull streaming responses

makeHttpSync
({
HttpSyncOptions.url: string

URL of the sync backend

@example

const syncBackend = makeHttpSync({ url: 'https://sync.example.com' })

url
: 'https://sync.example.com',
HttpSyncOptions.headers?: Record<string, string>
headers
: {
type Authorization: string
Authorization
: 'Bearer token',
'X-Custom-Header': 'value',
},
HttpSyncOptions.livePull?: {
pollInterval?: Input;
}
livePull
: {
pollInterval?: Input

How often to poll for new events

@default5 seconds

pollInterval
: 2000, // Poll every 2 seconds
},
})

Creates a Durable Object RPC-based sync backend (for internal use).

Options:

  • syncBackendStub - Durable Object stub implementing SyncBackendRpcInterface
  • durableObjectState - The client Durable Object’s ctx (DurableObjectState); scopes live-pull routing to this instance so it resets on reconstruction
  • durableObjectContext - Context for RPC callbacks:
    • bindingName - Wrangler binding name for the client DO
    • durableObjectId - Client Durable Object ID

Features:

  • Direct RPC communication
  • Real-time live pull via callbacks
  • Hibernation support

Handles the RPC callback for live-pull updates in Durable Objects. Pass the client DO’s ctx (DurableObjectState) so the update is routed to that instance’s live pull. The syncUpdateRpc method also receives the subscription’s storeId as a required trailing argument: a reconstructed (store-less) DO can re-boot its store from it and catch up rather than drop the update — see the Durable Object adapter’s recovery options.

import {
class DurableObject<Env = Cloudflare.Env, Props = {}>
DurableObject
} from 'cloudflare:workers'
import { type
(alias) interface ClientDoWithRpcCallback
import ClientDoWithRpcCallback
ClientDoWithRpcCallback
,
const createStoreDoPromise: <TSchema extends LiveStoreSchema, TEnv, TState extends DurableObjectState = DurableObjectState<unknown>>(options: CreateStoreDoOptions<TSchema, TEnv, TState>) => Promise<Store<TSchema, {}>>

Promise-based wrapper around createStoreDo for simpler async/await usage.

Equivalent to calling createStoreDo(options).pipe(Effect.runPromise) with logging configured automatically.

@example

import { createStoreDoPromise } from '@livestore/adapter-cloudflare'
export class MyDurableObject extends DurableObject {
async fetch(request: Request) {
const store = await createStoreDoPromise({
schema,
storeId: 'my-store',
clientId: this.ctx.id.toString(),
sessionId: 'do-session',
durableObject: { ctx: this.ctx, env: this.env, bindingName: 'MY_DO' },
syncBackendStub: this.env.SYNC_BACKEND_DO.get(syncBackendId),
})
// Use store...
}
}

createStoreDoPromise
} from '@livestore/adapter-cloudflare'
import {
function nanoid(size?: number): string

Generate secure URL-friendly unique ID.

By default, the ID will have 21 symbols to have a collision probability similar to UUID v4.

import { nanoid } from 'nanoid'
model.id = nanoid() //=> "Uakgb_J5m9g-0JDMbcJqL"

@paramsize Size of the ID. The default size is 21.

@returnsA random string.

nanoid
, type
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
, type
type Unsubscribe = () => void

Function returned by store.subscribe() to stop receiving updates.

Call this to unsubscribe from a query and release the associated resources.

@example

const unsubscribe = store.subscribe(todos$, (todos) => console.log(todos))
// Later...
unsubscribe()

Unsubscribe
} from '@livestore/livestore'
import {
const handleSyncUpdateRpc: (ctx: DurableObjectState, payload: Uint8Array<ArrayBuffer>) => Promise<void>

Routes an update from the sync backend into this client's live pull.

Only ctx and payload go here; storeId is for reloading your store on a rebuilt DO (see example).

import { DurableObject } from 'cloudflare:workers'
import { ClientDoWithRpcCallback } from '@livestore/common-cf'
export class MyDurableObject extends DurableObject implements ClientDoWithRpcCallback {
// ...
async syncUpdateRpc(payload: Uint8Array<ArrayBuffer>, storeId: string) {
await this.getStore(storeId)
return handleSyncUpdateRpc(this.ctx, payload)
}
}

handleSyncUpdateRpc
} from '@livestore/sync-cf/client'
import type {
import Env
Env
} from './env.ts'
import {
import schema
schema
,
import tables
tables
} from './schema.ts'
import {
import storeIdFromRequest
storeIdFromRequest
} from './shared.ts'
type
type AlarmInfo = {
isRetry: boolean;
retryCount: number;
}
AlarmInfo
= {
isRetry: boolean
isRetry
: boolean
retryCount: number
retryCount
: number
}
export class
class LiveStoreClientDO
LiveStoreClientDO
extends
class DurableObject<Env = Cloudflare.Env, Props = {}>
DurableObject
<
import Env
Env
> implements
(alias) interface ClientDoWithRpcCallback
import ClientDoWithRpcCallback
ClientDoWithRpcCallback
{
override
LiveStoreClientDO.__DURABLE_OBJECT_BRAND: never
__DURABLE_OBJECT_BRAND
: never =
var undefined
undefined
as never
private
LiveStoreClientDO.storeId: string | undefined
storeId
: string | undefined
private
LiveStoreClientDO.cachedStore: Store<any, {}> | undefined
cachedStore
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
<typeof
import schema
schema
> | undefined
private
LiveStoreClientDO.storeSubscription: Unsubscribe | undefined
storeSubscription
:
type Unsubscribe = () => void

Function returned by store.subscribe() to stop receiving updates.

Call this to unsubscribe from a query and release the associated resources.

@example

const unsubscribe = store.subscribe(todos$, (todos) => console.log(todos))
// Later...
unsubscribe()

Unsubscribe
| undefined
private readonly
LiveStoreClientDO.todosQuery: any
todosQuery
=
import tables
tables
.
any
todos
.
any
select
()
override async
LiveStoreClientDO.fetch(request: Request): Promise<Response>
fetch
(
request: Request<unknown, CfProperties<unknown>>
request
:
interface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
interface Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
> {
// @ts-expect-error TODO remove casts once CF types are fixed in https://github.com/cloudflare/workerd/issues/4811
this.
LiveStoreClientDO.storeId: string | undefined
storeId
=
import storeIdFromRequest
storeIdFromRequest
(
request: Request<unknown, CfProperties<unknown>>
request
)
const
const store: Store<any, {}>
store
= await this.
LiveStoreClientDO.getStore(): Promise<Store<any, {}>>
getStore
()
await this.
LiveStoreClientDO.subscribeToStore(): Promise<void>
subscribeToStore
()
const
const todos: unknown
todos
=
const store: Store<any, {}>
store
.
Store<any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(this.
LiveStoreClientDO.todosQuery: any
todosQuery
)
return new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
(
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
(
const todos: unknown
todos
, null, 2), {
ResponseInit.headers?: HeadersInit
headers
: { 'Content-Type': 'application/json' },
})
}
private async
LiveStoreClientDO.getStore(): Promise<Store<any, {}>>
getStore
() {
if (this.
LiveStoreClientDO.cachedStore: Store<any, {}> | undefined
cachedStore
!==
var undefined
undefined
) {
return this.
LiveStoreClientDO.cachedStore: Store<any, {}>
cachedStore
}
const
const storeId: string
storeId
= this.
LiveStoreClientDO.storeId: string | undefined
storeId
??
function nanoid(size?: number): string

Generate secure URL-friendly unique ID.

By default, the ID will have 21 symbols to have a collision probability similar to UUID v4.

import { nanoid } from 'nanoid'
model.id = nanoid() //=> "Uakgb_J5m9g-0JDMbcJqL"

@paramsize Size of the ID. The default size is 21.

@returnsA random string.

nanoid
()
const
const store: Store<any, {}>
store
= await
createStoreDoPromise<any, Env, DurableObjectState<unknown>>(options: CreateStoreDoOptions<any, Env, DurableObjectState<unknown>>): Promise<Store<any, {}>>

Promise-based wrapper around createStoreDo for simpler async/await usage.

Equivalent to calling createStoreDo(options).pipe(Effect.runPromise) with logging configured automatically.

@example

import { createStoreDoPromise } from '@livestore/adapter-cloudflare'
export class MyDurableObject extends DurableObject {
async fetch(request: Request) {
const store = await createStoreDoPromise({
schema,
storeId: 'my-store',
clientId: this.ctx.id.toString(),
sessionId: 'do-session',
durableObject: { ctx: this.ctx, env: this.env, bindingName: 'MY_DO' },
syncBackendStub: this.env.SYNC_BACKEND_DO.get(syncBackendId),
})
// Use store...
}
}

createStoreDoPromise
({
schema: any

LiveStore schema that defines state, migrations, and validators.

schema
,
storeId: string

Logical identifier for the store instance persisted inside the Durable Object.

storeId
,
clientId: string

Unique identifier for the client that owns the Durable Object instance.

clientId
: 'client-do',
sessionId: string

Identifier for the LiveStore session running inside the Durable Object.

sessionId
:
function nanoid(size?: number): string

Generate secure URL-friendly unique ID.

By default, the ID will have 21 symbols to have a collision probability similar to UUID v4.

import { nanoid } from 'nanoid'
model.id = nanoid() //=> "Uakgb_J5m9g-0JDMbcJqL"

@paramsize Size of the ID. The default size is 21.

@returnsA random string.

nanoid
(),
durableObject: {
ctx: DurableObjectState<unknown>;
env: Env;
bindingName: any;
}

Runtime details about the Durable Object this store runs inside. Needed for sync backend to call back to this instance.

durableObject
: {
// @ts-expect-error TODO remove once CF types are fixed in https://github.com/cloudflare/workerd/issues/4811
ctx: DurableObjectState<unknown>

Durable Object state handle (e.g. this.ctx).

ctx
: this.
CloudflareWorkersModule.DurableObject<Env, {}>.ctx: DurableObjectState<{}>
ctx
,
env: Env

Environment bindings associated with the Durable Object.

env
: this.
CloudflareWorkersModule.DurableObject<Env, {}>.env: Env
env
,
bindingName: any

Binding name Cloudflare uses to reach this Durable Object from other workers.

bindingName
: 'CLIENT_DO',
},
syncBackendStub: DurableObjectStub<SyncBackendRpcInterface>

RPC stub pointing at the sync backend Durable Object used for replication.

syncBackendStub
: this.
CloudflareWorkersModule.DurableObject<Env, {}>.env: Env
env
.
any
SYNC_BACKEND_DO
.
any
get
(this.
CloudflareWorkersModule.DurableObject<Env, {}>.env: Env
env
.
any
SYNC_BACKEND_DO
.
any
idFromName
(
const storeId: string
storeId
)),
livePull?: boolean

Enables live pull mode to receive sync updates via Durable Object RPC callbacks.

@defaultfalse

livePull
: true,
})
this.
LiveStoreClientDO.cachedStore: Store<any, {}> | undefined
cachedStore
=
const store: Store<any, {}>
store
return
const store: Store<any, {}>
store
}
private async
LiveStoreClientDO.subscribeToStore(): Promise<void>
subscribeToStore
() {
const
const store: Store<any, {}>
store
= await this.
LiveStoreClientDO.getStore(): Promise<Store<any, {}>>
getStore
()
if (this.
LiveStoreClientDO.storeSubscription: Unsubscribe | undefined
storeSubscription
===
var undefined
undefined
) {
this.
LiveStoreClientDO.storeSubscription: Unsubscribe | undefined
storeSubscription
=
const store: Store<any, {}>
store
.
Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>.subscribe: <readonly any[]>(query: Queryable<readonly any[]>, onUpdate: (value: readonly any[]) => void, options?: SubscribeOptions<readonly any[]> | undefined) => Unsubscribe (+1 overload)
subscribe
(this.
LiveStoreClientDO.todosQuery: any
todosQuery
, (
todos: readonly any[]
todos
:
interface ReadonlyArray<T>
ReadonlyArray
<typeof
import tables
tables
.
any
todos
.
any
Type
>) => {
var console: Console
console
.
Console.log(...data: any[]): void (+3 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`todos for store (${this.
LiveStoreClientDO.storeId: string | undefined
storeId
})`,
todos: readonly any[]
todos
)
})
}
await this.
CloudflareWorkersModule.DurableObject<Env, {}>.ctx: DurableObjectState<{}>
ctx
.
DurableObjectState<{}>.storage: DurableObjectStorage
storage
.
DurableObjectStorage.setAlarm(scheduledTime: number | Date, options?: DurableObjectSetAlarmOptions): Promise<void>
setAlarm
(
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
() + 1000)
}
override
LiveStoreClientDO.alarm(_alarmInfo?: AlarmInfo): void | Promise<void>
alarm
(
_alarmInfo: AlarmInfo | undefined
_alarmInfo
?:
type AlarmInfo = {
isRetry: boolean;
retryCount: number;
}
AlarmInfo
): void |
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void> {
return this.
LiveStoreClientDO.subscribeToStore(): Promise<void>
subscribeToStore
()
}
async
LiveStoreClientDO.syncUpdateRpc(payload: Uint8Array<ArrayBuffer>, storeId: string): Promise<void>

The sync backend calls this to deliver a live update; storeId lets a rebuilt DO reload its store before delivering. See the Cloudflare Durable Object adapter docs for the recovery options.

syncUpdateRpc
(
payload: Uint8Array<ArrayBuffer>
payload
:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike>

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
<
interface ArrayBuffer

Represents a raw buffer of binary data, which is used to store data for the different typed arrays. ArrayBuffers cannot be read from or written to directly, but can be passed to a typed array or DataView Object to interpret the raw buffer as needed.

ArrayBuffer
>,
storeId: string
storeId
: string) {
this.
LiveStoreClientDO.storeId: string | undefined
storeId
=
storeId: string
storeId
await this.
LiveStoreClientDO.getStore(): Promise<Store<any, {}>>
getStore
()
// @ts-expect-error TODO remove once CF types are fixed in https://github.com/cloudflare/workerd/issues/4811
await
function handleSyncUpdateRpc(ctx: DurableObjectState, payload: Uint8Array<ArrayBuffer>): Promise<void>

Routes an update from the sync backend into this client's live pull.

Only ctx and payload go here; storeId is for reloading your store on a rebuilt DO (see example).

import { DurableObject } from 'cloudflare:workers'
import { ClientDoWithRpcCallback } from '@livestore/common-cf'
export class MyDurableObject extends DurableObject implements ClientDoWithRpcCallback {
// ...
async syncUpdateRpc(payload: Uint8Array<ArrayBuffer>, storeId: string) {
await this.getStore(storeId)
return handleSyncUpdateRpc(this.ctx, payload)
}
}

handleSyncUpdateRpc
(this.
CloudflareWorkersModule.DurableObject<Env, {}>.ctx: DurableObjectState<{}>
ctx
,
payload: Uint8Array<ArrayBuffer>
payload
)
}
}

Creates a sync backend Durable Object class.

Options:

  • onPush? - Callback for push events: (message, context) => void | Promise<void>
  • onPushRes? - Callback for push responses: (message) => void | Promise<void>
  • onPull? - Callback for pull requests: (message, context) => void | Promise<void>
  • onPullRes? - Callback for pull responses: (message) => void | Promise<void>
  • storage? - Storage engine: { _tag: 'do-sqlite' } | { _tag: 'd1', binding: string } (default: do-sqlite)
  • enabledTransports? - Set of enabled transports: Set<'http' | 'ws' | 'do-rpc'>
  • otel? - OpenTelemetry configuration:
    • baseUrl? - OTEL endpoint URL
    • serviceName? - Service name for traces
import {
const makeDurableObject: MakeDurableObjectClass

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
} from '@livestore/sync-cf/cf-worker'
const
const hasUserId: (p: unknown) => p is {
userId: string;
}
hasUserId
= (
p: unknown
p
: unknown):
p: unknown
p
is {
userId: string
userId
: string } =>
typeof
p: unknown
p
=== 'object' &&
p: object | null
p
!==
var undefined
undefined
&&
p: object | null
p
!== null && 'userId' in
p: object
p
export class
class SyncBackendDO
SyncBackendDO
extends
function makeDurableObject(options?: MakeDurableObjectClassOptions): {
new (ctx: DoState, env: Env): DoObject<SyncBackendRpcInterface>;
}

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
({
onPush?: (message: PushRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPush
: async (
message: Struct.ReadonlySide<{
readonly batch: $Array<Struct<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}>>;
readonly backendId: Option<String>;
}, "Type">
message
, {
storeId: string
storeId
,
payload: Json | undefined
payload
}) => {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`Push to store ${
storeId: string
storeId
}:`,
message: Struct.ReadonlySide<{
readonly batch: $Array<Struct<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}>>;
readonly backendId: Option<String>;
}, "Type">
message
.
batch: readonly Struct.ReadonlySide<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}, "Type">[]
batch
)
// Custom business logic
if (
const hasUserId: (p: unknown) => p is {
userId: string;
}
hasUserId
(
payload: Json | undefined
payload
) === true) {
await
var Promise: PromiseConstructor

Represents the completion of an asynchronous operation

Promise
.
PromiseConstructor.resolve(): Promise<void> (+2 overloads)

Creates a new resolved promise.

@returnsA resolved promise.

resolve
()
}
},
onPull?: (message: PullRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPull
: async (
_message: Struct.ReadonlySide<{
readonly cursor: Option<Struct<{
readonly backendId: String;
readonly eventSequenceNumber: brand<Int, "GlobalEventSequenceNumber">;
}>>;
}, "Type">
_message
, {
storeId: string
storeId
}) => {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`Pull from store ${
storeId: string
storeId
}`)
},
enabledTransports?: Set<"http" | "ws" | "do-rpc">

Enabled transports for sync backend

  • http: HTTP JSON-RPC
  • ws: WebSocket
  • do-rpc: Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

@defaultSet(['http', 'ws', 'do-rpc'])

enabledTransports
: new
var Set: SetConstructor
new <"http" | "ws">(iterable?: Iterable<"http" | "ws"> | null | undefined) => Set<"http" | "ws"> (+1 overload)
Set
(['ws', 'http']), // Disable DO RPC
otel?: {
baseUrl?: string;
serviceName?: string;
}
otel
: {
baseUrl?: string
baseUrl
: 'https://otel.example.com',
serviceName?: string
serviceName
: 'livestore-sync',
},
}) {}

Creates a complete Cloudflare Worker for the sync backend.

Options:

  • syncBackendBinding - Durable Object binding name defined in wrangler.toml
  • validatePayload? - Payload validation function: (payload, context) => void | Promise<void>
  • enableCORS? - Enable CORS headers (default: false)

makeWorker is a quick way to get started in simple demos. In most production workers you typically want to share routing logic with other endpoints, so prefer wiring your own fetch handler and call handleSyncRequest when you detect a sync request. A minimal example:

import type {
type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada>, env: TEnv, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>;
}
CFWorker
,
import CfTypes
CfTypes
} from '@livestore/sync-cf/cf-worker'
import {
const handleSyncRequest: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown, TSyncPayload = Json>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }: {
request: CfTypes.Request<CFHostMetada>;
searchParams: SearchParams;
env?: TEnv | undefined;
ctx: CfTypes.ExecutionContext;
syncBackendBinding: MakeWorkerOptions<TEnv, TSyncPayload>["syncBackendBinding"];
headers?: CfTypes.HeadersInit | undefined;
validatePayload?: MakeWorkerOptions<TEnv, TSyncPayload>["validatePayload"];
syncPayloadSchema?: MakeWorkerOptions<TEnv, TSyncPayload>["syncPayloadSchema"];
}) => Promise<CfTypes.Response>

Handles LiveStore sync requests (e.g. with search params ?storeId=...&transport=...).

@example

Token-based authentication

const validatePayload = (payload: Schema.Json | undefined, context: { storeId: string }) => {
if (payload?.authToken !== 'insecure-token-change-me') {
throw new Error('Invalid auth token')
}
}

@example

Cookie-based authentication

const validatePayload = async (payload: Schema.Json | undefined, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

@throws{UnknownError} If the payload is invalid

handleSyncRequest
,
const matchSyncRequest: (request: CfTypes.Request) => SearchParams | undefined

Extracts the LiveStore sync search parameters from a request. Returns undefined when the request does not carry valid sync metadata so callers can fall back to custom routing.

matchSyncRequest
} from '@livestore/sync-cf/cf-worker'
import type {
import Env
Env
} from './env.ts'
export default {
fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada, CfTypes.CfProperties<CFHostMetada>>, env: Env, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>
fetch
: async (
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
:
import CfTypes
CfTypes
.
interface Request<CfHostMetadata = unknown, Cf = CfTypes.CfProperties<CfHostMetadata>>

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
,
env: Env
env
:
import Env
Env
,
ctx: CfTypes.ExecutionContext<unknown>
ctx
:
import CfTypes
CfTypes
.
interface ExecutionContext<Props = unknown>
ExecutionContext
) => {
const
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
=
function matchSyncRequest(request: CfTypes.Request): SearchParams | undefined

Extracts the LiveStore sync search parameters from a request. Returns undefined when the request does not carry valid sync metadata so callers can fall back to custom routing.

matchSyncRequest
(
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
)
if (
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
!==
var undefined
undefined
) {
return
handleSyncRequest<Env, undefined, unknown, Json>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }: {
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>;
searchParams: SearchParams;
env?: any;
ctx: CfTypes.ExecutionContext;
syncBackendBinding: any;
headers?: CfTypes.HeadersInit | undefined;
validatePayload?: ((payload: Json, context: ValidatePayloadContext) => void | Promise<void>) | undefined;
syncPayloadSchema?: Decoder<Json, never> | undefined;
}): Promise<CfTypes.Response>

Handles LiveStore sync requests (e.g. with search params ?storeId=...&transport=...).

@example

Token-based authentication

const validatePayload = (payload: Schema.Json | undefined, context: { storeId: string }) => {
if (payload?.authToken !== 'insecure-token-change-me') {
throw new Error('Invalid auth token')
}
}

@example

Cookie-based authentication

const validatePayload = async (payload: Schema.Json | undefined, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

@throws{UnknownError} If the payload is invalid

handleSyncRequest
({
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
,
searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
}
searchParams
,
env?: any
env
,
ctx: CfTypes.ExecutionContext<unknown>

Only there for type-level reasons

ctx
,
syncBackendBinding: any

Binding name of the sync backend Durable Object

syncBackendBinding
: 'SYNC_BACKEND_DO',
})
}
// Custom routes, assets, etc.
return new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
('Not found', {
ResponseInit.status?: number
status
: 404 }) as unknown as
import CfTypes
CfTypes
.
interface Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
},
} satisfies
type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada>, env: TEnv, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>;
}
CFWorker
<
import Env
Env
>
import {
const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends Rpc.DurableObjectBranded | undefined = undefined, TSyncPayload = Json>(options: MakeWorkerOptions<TEnv, TSyncPayload>) => CFWorker<TEnv, TDurableObjectRpc>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
} from '@livestore/sync-cf/cf-worker'
export default
makeWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined, Json>(options: MakeWorkerOptions<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, Json>): CFWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
({
syncBackendBinding: "SYNC_BACKEND_DO"

Binding name of the sync Durable Object declared in wrangler config.

syncBackendBinding
: 'SYNC_BACKEND_DO',
validatePayload?: (payload: Json, context: ValidatePayloadContext) => void | Promise<void>

Validates the (optionally decoded) payload during WebSocket connection establishment. If

syncPayloadSchema

is provided, payload will be of the schema's inferred type.

The context includes request headers for cookie-based or header-based authentication.

@example

Cookie-based authentication

validatePayload: async (payload, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

Note: This runs only at connection time, not for individual push events. For push event validation, use the onPush callback in the Durable Object.

validatePayload
: (
payload: Json
payload
, {
storeId: string
storeId
}) => {
// Simple token-based guard at connection time
const
const hasAuthToken: boolean
hasAuthToken
= typeof
payload: Json
payload
=== 'object' &&
payload: JsonArray | JsonObject | null
payload
!== null && 'authToken' in
payload: JsonArray | JsonObject
payload
if (
const hasAuthToken: boolean
hasAuthToken
=== false) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Missing auth token')
}
if ((
payload: JsonObject
payload
as any).
any
authToken
!== 'insecure-token-change-me') {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Invalid auth token')
}
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`Validated connection for store: ${
storeId: string
storeId
}`)
},
enableCORS?: boolean

@defaultfalse

enableCORS
: true,
})

Handles sync backend HTTP requests in custom workers.

Options:

  • request - The incoming request
  • searchParams - Parsed sync request parameters
  • env - Worker environment
  • ctx - Worker execution context
  • syncBackendBinding - Durable Object binding name defined in wrangler.toml
  • headers? - Response headers
  • validatePayload? - Payload validation function
import type {
type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada>, env: TEnv, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>;
}
CFWorker
,
import CfTypes
CfTypes
} from '@livestore/sync-cf/cf-worker'
import {
const handleSyncRequest: <TEnv extends Env = Env, TDurableObjectRpc extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined, CFHostMetada = unknown, TSyncPayload = Json>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }: {
request: CfTypes.Request<CFHostMetada>;
searchParams: SearchParams;
env?: TEnv | undefined;
ctx: CfTypes.ExecutionContext;
syncBackendBinding: MakeWorkerOptions<TEnv, TSyncPayload>["syncBackendBinding"];
headers?: CfTypes.HeadersInit | undefined;
validatePayload?: MakeWorkerOptions<TEnv, TSyncPayload>["validatePayload"];
syncPayloadSchema?: MakeWorkerOptions<TEnv, TSyncPayload>["syncPayloadSchema"];
}) => Promise<CfTypes.Response>

Handles LiveStore sync requests (e.g. with search params ?storeId=...&transport=...).

@example

Token-based authentication

const validatePayload = (payload: Schema.Json | undefined, context: { storeId: string }) => {
if (payload?.authToken !== 'insecure-token-change-me') {
throw new Error('Invalid auth token')
}
}

@example

Cookie-based authentication

const validatePayload = async (payload: Schema.Json | undefined, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

@throws{UnknownError} If the payload is invalid

handleSyncRequest
,
const matchSyncRequest: (request: CfTypes.Request) => SearchParams | undefined

Extracts the LiveStore sync search parameters from a request. Returns undefined when the request does not carry valid sync metadata so callers can fall back to custom routing.

matchSyncRequest
} from '@livestore/sync-cf/cf-worker'
import type {
import Env
Env
} from './env.ts'
export default {
fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada, CfTypes.CfProperties<CFHostMetada>>, env: Env, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>
fetch
: async (
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
:
import CfTypes
CfTypes
.
interface Request<CfHostMetadata = unknown, Cf = CfTypes.CfProperties<CfHostMetadata>>

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
,
env: Env
env
:
import Env
Env
,
ctx: CfTypes.ExecutionContext<unknown>
ctx
:
import CfTypes
CfTypes
.
interface ExecutionContext<Props = unknown>
ExecutionContext
) => {
const
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
=
function matchSyncRequest(request: CfTypes.Request): SearchParams | undefined

Extracts the LiveStore sync search parameters from a request. Returns undefined when the request does not carry valid sync metadata so callers can fall back to custom routing.

matchSyncRequest
(
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
)
if (
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
!==
var undefined
undefined
) {
return
handleSyncRequest<Env, undefined, unknown, Json>({ request, searchParams: { storeId, payload, transport }, env: explicitlyProvidedEnv, syncBackendBinding, headers, validatePayload, syncPayloadSchema, }: {
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>;
searchParams: SearchParams;
env?: any;
ctx: CfTypes.ExecutionContext;
syncBackendBinding: any;
headers?: CfTypes.HeadersInit | undefined;
validatePayload?: ((payload: Json, context: ValidatePayloadContext) => void | Promise<void>) | undefined;
syncPayloadSchema?: Decoder<Json, never> | undefined;
}): Promise<CfTypes.Response>

Handles LiveStore sync requests (e.g. with search params ?storeId=...&transport=...).

@example

Token-based authentication

const validatePayload = (payload: Schema.Json | undefined, context: { storeId: string }) => {
if (payload?.authToken !== 'insecure-token-change-me') {
throw new Error('Invalid auth token')
}
}

@example

Cookie-based authentication

const validatePayload = async (payload: Schema.Json | undefined, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

@throws{UnknownError} If the payload is invalid

handleSyncRequest
({
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
,
searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
}
searchParams
,
env?: any
env
,
ctx: CfTypes.ExecutionContext<unknown>

Only there for type-level reasons

ctx
,
syncBackendBinding: any

Binding name of the sync backend Durable Object

syncBackendBinding
: 'SYNC_BACKEND_DO',
headers?: CfTypes.HeadersInit | undefined
headers
: { 'X-Custom': 'header' },
validatePayload?: ((payload: Json, context: ValidatePayloadContext) => void | Promise<void>) | undefined
validatePayload
: (
payload: Json
payload
, {
storeId: string
storeId
}) => {
// Custom validation logic
if (!(typeof
payload: Json
payload
=== 'object' &&
payload: JsonArray | JsonObject | null
payload
!== null && 'authToken' in
payload: JsonArray | JsonObject
payload
)) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Missing auth token')
}
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
('Validating store',
storeId: string
storeId
)
},
})
}
return new
var Response: new (body?: BodyInit | null, init?: ResponseInit) => Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
('Not found', {
ResponseInit.status?: number
status
: 404 }) as unknown as
import CfTypes
CfTypes
.
interface Response

The Response interface of the Fetch API represents the response to a request.

MDN Reference

Response
},
} satisfies
type CFWorker<TEnv extends Env = Env, _T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined> = {
fetch: <CFHostMetada = unknown>(request: CfTypes.Request<CFHostMetada>, env: TEnv, ctx: CfTypes.ExecutionContext) => Promise<CfTypes.Response>;
}
CFWorker
<
import Env
Env
>

Parses and validates sync request search parameters.

Returns the decoded search params or undefined if the request is not a LiveStore sync request.

import type {
import CfTypes
CfTypes
} from '@livestore/sync-cf/cf-worker'
import {
const matchSyncRequest: (request: CfTypes.Request) => SearchParams | undefined

Extracts the LiveStore sync search parameters from a request. Returns undefined when the request does not carry valid sync metadata so callers can fall back to custom routing.

matchSyncRequest
} from '@livestore/sync-cf/cf-worker'
declare const
const request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
:
import CfTypes
CfTypes
.
interface Request<CfHostMetadata = unknown, Cf = CfTypes.CfProperties<CfHostMetadata>>

The Request interface of the Fetch API represents a resource request.

MDN Reference

Request
const
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
=
function matchSyncRequest(request: CfTypes.Request): SearchParams | undefined

Extracts the LiveStore sync search parameters from a request. Returns undefined when the request does not carry valid sync metadata so callers can fall back to custom routing.

matchSyncRequest
(
const request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
)
if (
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
!==
var undefined
undefined
) {
const {
const storeId: string
storeId
,
const payload: Json | undefined
payload
,
const transport: "http" | "ws"
transport
} =
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
}
searchParams
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`Sync request for store ${
const storeId: string
storeId
} via ${
const transport: "http" | "ws"
transport
}`)
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const payload: Json | undefined
payload
)
}

Configure your wrangler.toml for sync backend deployment (default: DO SQLite storage):

name = "livestore-sync"
main = "./src/worker.ts"
compatibility_date = "2025-05-07"
compatibility_flags = [
"enable_request_signal", # Required for HTTP streaming
]
[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

To use D1 instead of DO SQLite, add a D1 binding and reference it from makeDurableObject({ storage: { _tag: 'd1', binding: '...' } }):

[[d1_databases]]
binding = "DB"
database_name = "livestore-sync"
database_id = "your-database-id"

Required environment bindings:

import type {
import CfTypes
CfTypes
,
(alias) interface SyncBackendRpcInterface
import SyncBackendRpcInterface

Durable Object interface supporting the DO RPC protocol for DO <> DO syncing.

SyncBackendRpcInterface
} from '@livestore/sync-cf/cf-worker'
export interface
interface Env
Env
{
Env.SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface>
SYNC_BACKEND_DO
:
import CfTypes
CfTypes
.
class DurableObjectNamespace<T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined>
DurableObjectNamespace
<
(alias) interface SyncBackendRpcInterface
import SyncBackendRpcInterface

Durable Object interface supporting the DO RPC protocol for DO <> DO syncing.

SyncBackendRpcInterface
>
}

LiveStore identifies sync requests purely by search parameters; the request path does not matter. Use matchSyncRequest(request) to detect sync traffic.

Required search parameters:

ParamTypeRequiredDescription
storeIdstringYesTarget LiveStore identifier.
transport'ws' | 'http'YesTransport protocol selector.
payloadJSON (URI-encoded)NoArbitrary JSON used for auth/tenant routing; validated in validatePayload.

Examples (any path):

  • WebSocket: https://sync.example.com?storeId=abc&transport=ws (must include Upgrade: websocket)
  • HTTP: https://sync.example.com?storeId=abc&transport=http

Notes:

  • For transport=ws, if the request is not a WebSocket upgrade, the backend returns 426 Upgrade Required.
  • transport='do-rpc' is internal for Durable Object RPC and not exposed via URL parameters.

By default, events are stored in the Durable Object’s SQLite with tables following the pattern:

eventlog_{PERSISTENCE_FORMAT_VERSION}_{storeId}

You can opt into D1 with the same table shape. The persistence format version is automatically managed and incremented when the storage schema changes.

  • DO SQLite (default)
    • Pros: easiest deploy (no D1), data co-located with the DO, lowest latency
    • Cons: not directly inspectable outside the DO; operational tooling must go through the DO
  • D1 (optional)
    • Pros: inspectable using D1 tools/clients; enables cross-store analytics outside DOs
    • Cons: extra hop, JSON response size considerations; requires D1 provisioning

Deploy to Cloudflare Workers:

Terminal window
# Deploy the worker
npx wrangler deploy
# Create D1 database
npx wrangler d1 create livestore-sync
# Run migrations if needed
npx wrangler d1 migrations apply livestore-sync

Run locally with Wrangler:

Terminal window
# Start local development server
npx wrangler dev
# Access local D1 database
# Located at: .wrangler/state/d1/miniflare-D1DatabaseObject/XXX.sqlite
import {
const makeWorker: (options: WorkerOptions) => void
makeWorker
} from '@livestore/adapter-web/worker'
import {
const makeWsSync: (options: WsSyncOptions) => SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
} from '@livestore/sync-cf/client'
import {
import schema
schema
} from './schema.ts'
function makeWorker(options: WorkerOptions): void
makeWorker
({
schema: LiveStoreSchema<DbSchema, EventDefRecord>
schema
,
sync?: SyncOptions
sync
: {
backend?: SyncBackendConstructor<any, Json>
backend
:
function makeWsSync(options: WsSyncOptions): SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
({
WsSyncOptions.url: string

URL of the sync backend

The protocol can either http/https or ws/wss

url
: 'wss://sync.example.com',
}),
},
})
import {
const makeDurableObject: MakeDurableObjectClass

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
,
const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends Rpc.DurableObjectBranded | undefined = undefined, TSyncPayload = Json>(options: MakeWorkerOptions<TEnv, TSyncPayload>) => CFWorker<TEnv, TDurableObjectRpc>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
} from '@livestore/sync-cf/cf-worker'
export class
class SyncBackendDO
SyncBackendDO
extends
function makeDurableObject(options?: MakeDurableObjectClassOptions): {
new (ctx: DoState, env: Env): DoObject<SyncBackendRpcInterface>;
}

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
({
onPush?: (message: PushRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPush
: async (
message: Struct.ReadonlySide<{
readonly batch: $Array<Struct<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}>>;
readonly backendId: Option<String>;
}, "Type">
message
, {
storeId: string
storeId
}) => {
// Log all sync events
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`Store ${
storeId: string
storeId
} received ${
message: Struct.ReadonlySide<{
readonly batch: $Array<Struct<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}>>;
readonly backendId: Option<String>;
}, "Type">
message
.
batch: readonly Struct.ReadonlySide<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}, "Type">[]
batch
.
ReadonlyArray<Struct<Fields extends Struct.Fields>.ReadonlySide<{ readonly name: String; readonly args: Any; readonly seqNum: brand<Int, "GlobalEventSequenceNumber">; readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">; readonly clientId: String; readonly sessionId: String; }, "Type">>.length: number

Gets the length of the array. This is a number one higher than the highest element defined in an array.

length
} events`)
},
}) {}
const
const hasStoreAccess: (_userId: string, _storeId: string) => boolean
hasStoreAccess
= (
_userId: string
_userId
: string,
_storeId: string
_storeId
: string): boolean => true
export default
makeWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined, Json>(options: MakeWorkerOptions<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, Json>): CFWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
({
syncBackendBinding: "SYNC_BACKEND_DO"

Binding name of the sync Durable Object declared in wrangler config.

syncBackendBinding
: 'SYNC_BACKEND_DO',
validatePayload?: (payload: Json, context: ValidatePayloadContext) => void | Promise<void>

Validates the (optionally decoded) payload during WebSocket connection establishment. If

syncPayloadSchema

is provided, payload will be of the schema's inferred type.

The context includes request headers for cookie-based or header-based authentication.

@example

Cookie-based authentication

validatePayload: async (payload, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

Note: This runs only at connection time, not for individual push events. For push event validation, use the onPush callback in the Durable Object.

validatePayload
: (
payload: Json
payload
, {
storeId: string
storeId
}) => {
if (!(typeof
payload: Json
payload
=== 'object' &&
payload: JsonArray | JsonObject | null
payload
!== null && 'userId' in
payload: JsonArray | JsonObject
payload
)) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('User ID required')
}
// Validate user has access to store
if (
const hasStoreAccess: (_userId: string, _storeId: string) => boolean
hasStoreAccess
((
payload: JsonObject
payload
as any).
any
userId
as string,
storeId: string
storeId
) === false) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Unauthorized access to store')
}
},
enableCORS?: boolean

@defaultfalse

enableCORS
: true,
})
import {
const makeDurableObject: MakeDurableObjectClass

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
} from '@livestore/sync-cf/cf-worker'
type
type Transport = "http" | "ws" | "do-rpc"
Transport
= 'http' | 'ws' | 'do-rpc'
const
const getTransportFromContext: (ctx: unknown) => Transport
getTransportFromContext
= (
ctx: unknown
ctx
: unknown):
type Transport = "http" | "ws" | "do-rpc"
Transport
=> {
if (typeof
ctx: unknown
ctx
=== 'object' &&
ctx: object | null
ctx
!== null && 'transport' in (
ctx: object
ctx
as any)) {
const
const t: any
t
= (
ctx: object
ctx
as any).
any
transport
if (
const t: any
t
=== 'http' ||
const t: any
t
=== 'ws' ||
const t: any
t
=== 'do-rpc') return
const t: any
t
}
return 'http'
}
export class
class SyncBackendDO
SyncBackendDO
extends
function makeDurableObject(options?: MakeDurableObjectClassOptions): {
new (ctx: DoState, env: Env): DoObject<SyncBackendRpcInterface>;
}

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
({
// Enable all transport modes
enabledTransports?: Set<"http" | "ws" | "do-rpc">

Enabled transports for sync backend

  • http: HTTP JSON-RPC
  • ws: WebSocket
  • do-rpc: Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

@defaultSet(['http', 'ws', 'do-rpc'])

enabledTransports
: new
var Set: SetConstructor
new <Transport>(iterable?: Iterable<Transport> | null | undefined) => Set<Transport> (+1 overload)
Set
<
type Transport = "http" | "ws" | "do-rpc"
Transport
>(['http', 'ws', 'do-rpc']),
onPush?: (message: PushRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPush
: async (
message: Struct.ReadonlySide<{
readonly batch: $Array<Struct<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}>>;
readonly backendId: Option<String>;
}, "Type">
message
,
context: CallbackContext
context
) => {
const
const transport: Transport
transport
=
const getTransportFromContext: (ctx: unknown) => Transport
getTransportFromContext
(
context: CallbackContext
context
)
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(`Push via ${
const transport: Transport
transport
}:`,
message: Struct.ReadonlySide<{
readonly batch: $Array<Struct<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}>>;
readonly backendId: Option<String>;
}, "Type">
message
.
batch: readonly Struct.ReadonlySide<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}, "Type">[]
batch
.
ReadonlyArray<Struct<Fields extends Struct.Fields>.ReadonlySide<{ readonly name: String; readonly args: Any; readonly seqNum: brand<Int, "GlobalEventSequenceNumber">; readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">; readonly clientId: String; readonly sessionId: String; }, "Type">>.length: number

Gets the length of the array. This is a number one higher than the highest element defined in an array.

length
)
},
}) {}