Configure LiveStore for web browsers, Expo / React Native, Node.js, and Cloudflare Durable Objects using the appropriate platform adapter.
Platform adapters connect LiveStore to the underlying storage and runtime for each environment. Choose the adapter that matches your deployment target.
Android Chrome does not support SharedWorker. LiveStore automatically falls back to single-tab mode on Android Chrome. You can also opt in explicitly:
main.ts
import { makeSingleTabAdapter } from '@livestore/adapter-web'import LiveStoreWorker from './livestore.worker.ts?worker'// Use only when you specifically need single-tab mode.// Prefer makePersistedAdapter which auto-detects SharedWorker support.const adapter = makeSingleTabAdapter({ worker: LiveStoreWorker, storage: { type: 'opfs' },})
LiveStore persists data to OPFS. In Safari and Firefox private browsing mode, OPFS is unavailable and LiveStore falls back to in-memory storage automatically. Sync still works in this fallback mode; data is just not saved across reloads.Detect the current storage mode via store.storageMode:
if (store.storageMode === 'in-memory') { // warn the user that data won't persist}
Dedicated web worker (leader worker) — acts as the single writer for storage and handles the sync backend connection. Required for synchronous OPFS access.
Shared worker — acts as a proxy between browser tabs, enabling multi-tab synchronization via a binary message channel.
Make sure your schema file does not import code that runs on the main thread (such as React components). If needed, add import '@livestore/adapter-web/worker-vite-dev-polyfill' to your worker file as a workaround.
Required browser APIs: OPFS, navigator.locks, WebAssembly. SharedWorker is used for multi-tab sync but is not strictly required (single-tab mode is the fallback).Bundle size: approximately 180 KB (LiveStore, gzipped) + 300 KB (SQLite WASM, gzipped).
The Expo adapter enables LiveStore in React Native apps built with Expo. It uses native SQLite via expo-sqlite for high-performance local storage on iOS and Android.
Android blocks http:// and ws:// connections by default. For local development against a non-HTTPS sync backend, install expo-build-properties and configure app.json:
The Expo adapter runs LiveStore directly in the main JavaScript thread, using native SQLite bindings from expo-sqlite. This differs from the web adapter, which offloads work to a web worker.
import type { ClientDoWithRpcCallback } from '@livestore/adapter-cloudflare'import type { CfTypes, SyncBackendRpcInterface } from '@livestore/sync-cf/cf-worker'export type Env = { CLIENT_DO: CfTypes.DurableObjectNamespace<ClientDoWithRpcCallback> SYNC_BACKEND_DO: CfTypes.DurableObjectNamespace<SyncBackendRpcInterface> DB: CfTypes.D1Database}
2
Create the sync backend Durable Object
The sync backend handles event distribution between clients:
sync-backend.ts
import * as SyncBackend from '@livestore/sync-cf/cf-worker'export class SyncBackendDO extends SyncBackend.makeDurableObject({ // Optional: handle push events // onPush: async (message, { storeId }) => { // console.log(`onPush for store (${storeId})`, message.batch) // },}) {}
3
Create the client Durable Object
Each client Durable Object hosts a LiveStore instance and exposes DO RPC callbacks:
client-do.ts
/// <reference types="@cloudflare/workers-types" />import { DurableObject } from 'cloudflare:workers'import { type ClientDoWithRpcCallback, createStoreDoPromise } from '@livestore/adapter-cloudflare'import { nanoid, type Store, type Unsubscribe } from '@livestore/livestore'import { handleSyncUpdateRpc } from '@livestore/sync-cf/client'import type { Env } from './env.ts'import { schema, tables } from './schema.ts'import { storeIdFromRequest } from './shared.ts'export class LiveStoreClientDO extends DurableObject<Env> implements ClientDoWithRpcCallback { override __DURABLE_OBJECT_BRAND: never = undefined as never private storeId: string | undefined private cachedStore: Store<typeof schema> | undefined private storeSubscription: Unsubscribe | undefined private readonly todosQuery = tables.todos.select() override async fetch(request: Request): Promise<Response> { this.storeId = storeIdFromRequest(request) const store = await this.getStore() const todos = store.query(this.todosQuery) return new Response(JSON.stringify(todos, null, 2), { headers: { 'Content-Type': 'application/json' }, }) } private async getStore() { if (this.cachedStore !== undefined) return this.cachedStore const storeId = this.storeId ?? nanoid() const store = await createStoreDoPromise({ schema, storeId, clientId: 'client-do', sessionId: nanoid(), durableObject: { ctx: this.ctx, env: this.env, bindingName: 'CLIENT_DO', }, syncBackendStub: this.env.SYNC_BACKEND_DO.get( this.env.SYNC_BACKEND_DO.idFromName(storeId) ), livePull: true, }) this.cachedStore = store return store } async syncUpdateRpc(payload: unknown) { await handleSyncUpdateRpc(payload) }}
4
Set up the worker fetch handler
Route incoming requests to the sync backend or client Durable Object:
worker.ts
import type { CfTypes } from '@livestore/sync-cf/cf-worker'import * as SyncBackend from '@livestore/sync-cf/cf-worker'import type { Env } from './env.ts'import { storeIdFromRequest } from './shared.ts'export default { fetch: async (request: CfTypes.Request, env: Env, ctx: CfTypes.ExecutionContext) => { const url = new URL(request.url) const searchParams = SyncBackend.matchSyncRequest(request) if (searchParams !== undefined) { return SyncBackend.handleSyncRequest({ request, searchParams, env, ctx, syncBackendBinding: 'SYNC_BACKEND_DO', headers: {}, }) } if (url.pathname.endsWith('/client-do') === true) { const storeId = storeIdFromRequest(request) const id = env.CLIENT_DO.idFromName(storeId) return env.CLIENT_DO.get(id).fetch(request) } return new Response('Not found', { status: 404 }) as unknown as CfTypes.Response },} satisfies SyncBackend.CFWorker<Env>
Client Durable Objects must implement this method so the sync backend can deliver live updates. Forward the payload to handleSyncUpdateRpc from @livestore/sync-cf/client.
To wipe Durable Object databases during development, enable resetPersistence in createStoreDoPromise and guard it behind a protected route or admin token.
Resetting persistence deletes all LiveStore state and eventlog data in the Durable Object. Never expose this to production traffic.