Skip to content

Cloudflare Durable Object adapter

The Cloudflare Durable Object adapter enables running LiveStore applications on Cloudflare Workers with stateful Durable Objects for synchronized real-time data.

Terminal window
pnpm add @livestore/adapter-cloudflare @livestore/sync-cf

Configure your wrangler.toml with the required Durable Object bindings:

name = "my-livestore-app"
main = "./src/worker.ts"
compatibility_date = "2025-05-07"
compatibility_flags = [
"enable_request_signal", # Required for HTTP RPC streams
]
[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[durable_objects.bindings]]
name = "CLIENT_DO"
class_name = "LiveStoreClientDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO", "LiveStoreClientDO"]
[[d1_databases]]
binding = "DB"
database_name = "my-livestore-db"
database_id = "your-database-id"

Define your Worker bindings so TypeScript can guide you when wiring Durable Objects:

import type {
(alias) interface ClientDoWithRpcCallback
import ClientDoWithRpcCallback
ClientDoWithRpcCallback
} from '@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'
export type
type Env = {
CLIENT_DO: CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>;
SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface>;
DB: CfTypes.D1Database;
}
Env
= {
type CLIENT_DO: CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback>
CLIENT_DO
:
import CfTypes
CfTypes
.
class DurableObjectNamespace<T extends CfTypes.Rpc.DurableObjectBranded | undefined = undefined>
DurableObjectNamespace
<
(alias) interface ClientDoWithRpcCallback
import ClientDoWithRpcCallback
ClientDoWithRpcCallback
>
type 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
>
type DB: CfTypes.D1Database
DB
:
import CfTypes
CfTypes
.
class D1Database
D1Database
}

We also use a small helper to extract the store identifier from incoming requests:

import type {
import CfTypes
CfTypes
} from '@livestore/sync-cf/cf-worker'
export const
const storeIdFromRequest: (request: CfTypes.Request) => string
storeIdFromRequest
= (
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 url: URL
url
= new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.

MDN Reference

URL
(
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
.
Request<unknown, CfProperties<unknown>>.url: string

The url read-only property of the Request interface contains the URL of the request.

MDN Reference

url
)
const
const storeId: string | null
storeId
=
const url: URL
url
.
URL.searchParams: URLSearchParams

The searchParams read-only property of the URL interface returns a URLSearchParams object allowing access to the GET decoded query arguments contained in the URL.

MDN Reference

searchParams
.
URLSearchParams.get(name: string): string | null

The get() method of the URLSearchParams interface returns the first value associated to the given search parameter.

MDN Reference

get
('storeId')
if (
const storeId: string | null
storeId
=== null) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('storeId is required in URL search params')
}
return
const storeId: string
storeId
}

The sync backend handles pushing and pulling events between clients:

import * as
import SyncBackend
SyncBackend
from '@livestore/sync-cf/cf-worker'
export class
class SyncBackendDO
SyncBackendDO
extends
import SyncBackend
SyncBackend
.
const makeDurableObject: (options?: SyncBackend.MakeDurableObjectClassOptions) => {
new (ctx: SyncBackend.DoState, env: SyncBackend.Env): SyncBackend.DoObject<SyncBackend.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
({
// Optional: Handle push events
// onPush: async (message, { storeId }) => {
// console.log(`onPush for store (${storeId})`, message.batch)
// },
}) {}

Each client Durable Object hosts a LiveStore instance and exposes DO RPC callbacks:

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
)
}
}

The worker routes incoming requests either to the sync backend or to the client Durable Object:

import type {
import CfTypes
CfTypes
} from '@livestore/sync-cf/cf-worker'
import * as
import SyncBackend
SyncBackend
from '@livestore/sync-cf/cf-worker'
import type {
import Env
Env
} from './env.ts'
import {
import storeIdFromRequest
storeIdFromRequest
} from './shared.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 url: URL
url
= new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.

MDN Reference

URL
(
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
.
Request<unknown, CfProperties<unknown>>.url: string

The url read-only property of the Request interface contains the URL of the request.

MDN Reference

url
)
const
const searchParams: {
readonly transport: "http" | "ws";
readonly storeId: string;
readonly payload?: Json | undefined;
} | undefined
searchParams
=
import SyncBackend
SyncBackend
.
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
(
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
import SyncBackend
SyncBackend
.
const 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: SyncBackend.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
: {},
})
}
if (
const url: URL
url
.
URL.pathname: string

The pathname property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character.

MDN Reference

pathname
.
String.endsWith(searchString: string, endPosition?: number): boolean

Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding elements of this object (converted to a String) starting at endPosition – length(this). Otherwise returns false.

endsWith
('/client-do') === true) {
const
const storeId: any
storeId
=
import storeIdFromRequest
storeIdFromRequest
(
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
)
const
const id: any
id
=
env: Env
env
.
any
CLIENT_DO
.
any
idFromName
(
const storeId: any
storeId
)
return
env: Env
env
.
any
CLIENT_DO
.
any
get
(
const id: any
id
).
any
fetch
(
request: CfTypes.Request<unknown, CfTypes.CfProperties<unknown>>
request
)
}
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
import SyncBackend
SyncBackend
.
type CFWorker<TEnv extends SyncBackend.Env = SyncBackend.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
>

Creates a LiveStore instance inside a Durable Object.

Options:

  • schema – LiveStore schema definition
  • storeId – Unique identifier for the store
  • clientId – Client identifier
  • sessionId – Session identifier (use nanoid())
  • durableObject – Context about the Durable Object hosting the store:
    • state – Durable Object state handle (for example this.ctx)
    • env – Environment bindings for the Durable Object
    • bindingName – Name other workers use to reach this Durable Object
  • syncBackendStub – Durable Object stub used to reach the sync backend
  • livePull – Enable real-time updates (default: false)
  • resetPersistence – Drop LiveStore state/eventlog persistence before booting (development only, default: false)
  • logger? – Optional Effect logger layer to customize formatting/output
  • logLevel? – Optional minimum log level (use "None" to disable logs)

Client Durable Objects must implement this method so the sync backend can deliver live updates to them. Forward the DO’s ctx and the payload to handleSyncUpdateRpc(ctx, payload) (see the client Durable Object example above).

Cloudflare can evict a Durable Object at any time and start a fresh instance on the next request. Because this call is what wakes the DO, it can arrive at an instance that no longer has your store in memory — and a fresh instance can’t tell which storeId it belongs to on its own. That’s why the sync backend also passes the storeId. You choose how to use it:

  • Eager (stay in sync): load the store before delivering — set this.storeId = storeId, then await this.getStore() (idempotent, and it catches up on any updates the DO missed while it was gone). The DO stays in sync whenever it is awake and costs nothing while idle.
  • Reactive (recover lazily): ignore storeId (you can omit the parameter). The DO drops that one live update and picks up the changes the next time your code loads the store (e.g. the next fetch). Simpler, but live updates are not applied while the DO is idle.

Pass storeId to your store load (getStore), not to handleSyncUpdateRpc — that function only needs ctx and payload.

Resetting LiveStore persistence (development only)

Section titled “Resetting LiveStore persistence (development only)”

When iterating locally, you can instruct the adapter to wipe the Durable Object’s LiveStore databases before booting by enabling resetPersistence. Guard this behind a protected route or admin token.

import {
const createStoreDoPromise: <TSchema extends LiveStoreSchema, TEnv, TState extends CfTypes.DurableObjectState = CfTypes.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
} from '@livestore/livestore'
import type {
import CfTypes
CfTypes
} from '@livestore/sync-cf/cf-worker'
import type {
import Env
Env
} from './env.ts'
import {
import schema
schema
} from './schema.ts'
export const
const maybeResetStore: ({ request, env, ctx, }: {
request: Request;
env: Env;
ctx: CfTypes.DurableObjectState;
}) => Promise<Store<any, {}>>
maybeResetStore
= async ({
request: Request<unknown, CfProperties<unknown>>
request
,
env: Env
env
,
ctx: CfTypes.DurableObjectState<unknown>
ctx
,
}: {
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
env: Env
env
:
import Env
Env
ctx: CfTypes.DurableObjectState<unknown>
ctx
:
import CfTypes
CfTypes
.
interface DurableObjectState<Props = unknown>
DurableObjectState
}) => {
const
const url: URL
url
= new
var URL: new (url: string | URL, base?: string | URL) => URL

The URL interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.

MDN Reference

URL
(
request: Request<unknown, CfProperties<unknown>>
request
.
Request<unknown, CfProperties<unknown>>.url: string

The url read-only property of the Request interface contains the URL of the request.

MDN Reference

url
)
const
const shouldReset: boolean
shouldReset
=
const url: URL
url
.
URL.pathname: string

The pathname property of the URL interface represents a location in a hierarchical structure. It is a string constructed from a list of path segments, each of which is prefixed by a / character.

MDN Reference

pathname
=== '/internal/livestore-dev-reset'
const
const storeId: string
storeId
=
const url: URL
url
.
URL.searchParams: URLSearchParams

The searchParams read-only property of the URL interface returns a URLSearchParams object allowing access to the GET decoded query arguments contained in the URL.

MDN Reference

searchParams
.
URLSearchParams.get(name: string): string | null (+1 overload)

The get() method of the URLSearchParams interface returns the first value associated to the given search parameter.

MDN Reference

get
('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, CfTypes.DurableObjectState<unknown>>(options: CreateStoreDoOptions<any, Env, CfTypes.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: CfTypes.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
: {
ctx: CfTypes.DurableObjectState<unknown>

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

ctx
,
env: Env

Environment bindings associated with the Durable Object.

env
,
bindingName: any

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

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

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

syncBackendStub
:
env: Env
env
.
any
SYNC_BACKEND_DO
.
any
get
(
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,
resetPersistence?: boolean

Clears existing Durable Object persistence before bootstrapping the store.

Note: Only use this for development purposes.

resetPersistence
:
const shouldReset: boolean
shouldReset
,
})
return
const store: Store<any, {}>
store
}
  • Use livePull: true to receive push-based updates via Durable Object RPC callbacks.
  • Subscribe to data changes inside the Durable Object to trigger side effects (see the client Durable Object example).
  • Wire additional routes in the worker fetch handler to expose debugging endpoints or admin operations.

For sync backend-related APIs like makeDurableObject, handleSyncRequest, and matchSyncRequest, see the Cloudflare sync provider documentation.