Skip to content

Web Adapter

Terminal window
npm install @livestore/adapter-web @livestore/wa-sqlite
import {
const makePersistedAdapter: (options: WebAdapterOptions) => Adapter

Creates a web adapter with persistent storage (currently only supports OPFS). Requires both a web worker and a shared worker.

@example

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreWorker from './livestore.worker.ts?worker'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
const adapter = makePersistedAdapter({
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
storage: { type: 'opfs' },
})

makePersistedAdapter
} from '@livestore/adapter-web'
import
const LiveStoreSharedWorker: new (options?: {
name?: string;
}) => SharedWorker
LiveStoreSharedWorker
from '@livestore/adapter-web/shared-worker?sharedworker'
const
const adapter: Adapter
adapter
=
function makePersistedAdapter(options: WebAdapterOptions): Adapter

Creates a web adapter with persistent storage (currently only supports OPFS). Requires both a web worker and a shared worker.

@example

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreWorker from './livestore.worker.ts?worker'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
const adapter = makePersistedAdapter({
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
storage: { type: 'opfs' },
})

makePersistedAdapter
({
storage: {
readonly type: "opfs";
readonly directory?: string | undefined;
}

Specifies where to persist data for this adapter

storage
: {
type: "opfs"
type
: 'opfs' },
worker: ((options: {
name: string;
}) => globalThis.Worker) | (new (options: {
name: string;
}) => globalThis.Worker)
worker
:
const LiveStoreWorker: new (options?: {
name?: string;
}) => Worker
LiveStoreWorker
,
sharedWorker: ((options: {
name: string;
}) => globalThis.SharedWorker) | (new (options: {
name: string;
}) => globalThis.SharedWorker)

This is mostly an implementation detail and needed to be exposed into app code due to a current Vite limitation (https://github.com/vitejs/vite/issues/8427).

In most cases this should look like:

import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
const adapter = makePersistedAdapter({
sharedWorker: LiveStoreSharedWorker,
// ...
})

sharedWorker
:
const LiveStoreSharedWorker: new (options?: {
name?: string;
}) => SharedWorker
LiveStoreSharedWorker
,
})
import {
const makeWorker: (options: WorkerOptions) => void
makeWorker
} from '@livestore/adapter-web/worker'
import {
const schema: FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>;
};
state: InternalState;
}>
schema
} from './schema/index.ts'
function makeWorker(options: WorkerOptions): void
makeWorker
({
schema: LiveStoreSchema<DbSchema, EventDefRecord>
schema
})
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 {
const schema: FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>;
};
state: InternalState;
}>
schema
} from './schema/index.ts'
function makeWorker(options: WorkerOptions): void
makeWorker
({
schema: LiveStoreSchema<DbSchema, EventDefRecord>
schema
,
sync?: SyncOptions
sync
: {
backend?: SyncBackendConstructor<any, JsonValue>
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
: 'ws://localhost:8787' }) } })

You can also use the in-memory adapter which can be useful in certain scenarios (e.g. testing).

import {
const makeInMemoryAdapter: (options?: InMemoryAdapterOptions) => Adapter

Create a web-only in-memory LiveStore adapter.

  • Runs entirely in memory: fast, zero I/O, great for tests, sandboxes, or ephemeral sessions.
  • Works across browser execution contexts: Window, WebWorker, SharedWorker, and ServiceWorker.
  • DevTools: to inspect this adapter from the browser DevTools, provide a sharedWorker in options.devtools. (The shared worker is used to bridge the DevTools UI to the running session.)
  • No persistence support: nothing is written to OPFS/IndexedDB/localStorage. importSnapshot can bootstrap initial state only; subsequent changes are not persisted anywhere.

makeInMemoryAdapter
} from '@livestore/adapter-web'
  • Make sure your schema doesn’t depend on any code which needs to run in the main thread (e.g. avoid importing from files using React)
    • Unfortunately this constraints you from co-locating your table definitions in component files.
    • You might be able to still work around this by using the following import in your worker:
      import '@livestore/adapter-web/worker-vite-dev-polyfill'

Why is there a dedicated web worker and a shared worker?

Section titled “Why is there a dedicated web worker and a shared worker?”
  • Shared worker:
    • Needed to allow tabs to communicate with each other using a binary message channel.
    • The shared worker mostly acts as a proxy to the dedicated web worker.
  • Dedicated web worker (also called “leader worker” via leader election mechanism using web locks):
    • Acts as the leader/single writer for the storage.
    • Also handles connection to sync backend.
    • Currently needed for synchronous OPFS API which isn’t supported in a shared worker. (Hopefully won’t be needed in the future anymore.)
  • While service workers seem similar to shared workers (i.e. only a single instance across all tabs), they serve different purposes and have different trade-offs.
  • Service workers are meant to be used to intercept network requests and tend to “shut down” when there are no requests for some period of time making them unsuitable for our use case.
  • Also note that service workers don’t support some needed APIs such as OPFS.

LiveStore currently only support OPFS to locally persist its data. In the future we might add support for other storage types (e.g. IndexedDB).

During development (NODE_ENV !== 'production'), LiveStore automatically copies older state database files into archive/ inside the OPFS directory for the store (e.g. livestore-<storeId>@<version>/archive/). The three most recent copies are retained so you can inspect pre-migration data; older archives are pruned. In production, we delete outdated state databases immediately.

LiveStore also uses window.sessionStorage to retain the identity of a client session (e.g. tab/window) across reloads.

Resetting local persistence only clears data stored in the browser and does not affect any connected sync backend.

In case you want to reset the local persistence of a client, you can provide the resetPersistence option to the adapter.

import {
const makePersistedAdapter: (options: WebAdapterOptions) => Adapter

Creates a web adapter with persistent storage (currently only supports OPFS). Requires both a web worker and a shared worker.

@example

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreWorker from './livestore.worker.ts?worker'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
const adapter = makePersistedAdapter({
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
storage: { type: 'opfs' },
})

makePersistedAdapter
} from '@livestore/adapter-web'
import
const LiveStoreSharedWorker: new (options?: {
name?: string;
}) => SharedWorker
LiveStoreSharedWorker
from '@livestore/adapter-web/shared-worker?sharedworker'
const
const resetPersistence: boolean
resetPersistence
= import.

The type of import.meta.

If you need to declare that a given property exists on import.meta, this type may be augmented via interface merging.

meta
.
ImportMeta.env: Bun.Env & NodeJS.ProcessEnv & ImportMetaEnv

The environment variables of the process

import.meta.env === process.env

env
.
ImportMetaEnv.DEV: boolean
DEV
&& new
var URLSearchParams: new (init?: string[][] | Record<string, string> | string | URLSearchParams) => URLSearchParams

The URLSearchParams interface defines utility methods to work with the query string of a URL.

MDN Reference

URLSearchParams
(
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
location: Location

The Window.location read-only property returns a Location object with information about the current location of the document.

MDN Reference

location
.
Location.search: string

The search property of the Location interface is a search string, also called a query string, that is a string containing a '?' followed by the parameters of the location's URL.

MDN Reference

search
).
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
('reset') !== null
if (
const resetPersistence: boolean
resetPersistence
) {
const
const searchParams: URLSearchParams
searchParams
= new
var URLSearchParams: new (init?: string[][] | Record<string, string> | string | URLSearchParams) => URLSearchParams

The URLSearchParams interface defines utility methods to work with the query string of a URL.

MDN Reference

URLSearchParams
(
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
location: Location

The Window.location read-only property returns a Location object with information about the current location of the document.

MDN Reference

location
.
Location.search: string

The search property of the Location interface is a search string, also called a query string, that is a string containing a '?' followed by the parameters of the location's URL.

MDN Reference

search
)
const searchParams: URLSearchParams
searchParams
.
URLSearchParams.delete(name: string, value?: string): void

The delete() method of the URLSearchParams interface deletes specified parameters and their associated value(s) from the list of all search parameters.

MDN Reference

delete
('reset')
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
history: History

The Window.history read-only property returns a reference to the History object, which provides an interface for manipulating the browser session history (pages visited in the tab or frame that the current page is loaded in).

MDN Reference

history
.
History.replaceState(data: any, unused: string, url?: string | URL | null): void

The replaceState() method of the History interface modifies the current history entry, replacing it with the state object and URL passed in the method parameters.

MDN Reference

replaceState
(null, '', `${
var window: Window & typeof globalThis

The window property of a Window object points to the window object itself.

MDN Reference

window
.
location: Location

The Window.location read-only property returns a Location object with information about the current location of the document.

MDN Reference

location
.
Location.pathname: string

The pathname property of the Location interface is a string containing the path of the URL for the location.

MDN Reference

pathname
}?${
const searchParams: URLSearchParams
searchParams
.
URLSearchParams.toString(): string
toString
()}`)
}
const
const adapter: Adapter
adapter
=
function makePersistedAdapter(options: WebAdapterOptions): Adapter

Creates a web adapter with persistent storage (currently only supports OPFS). Requires both a web worker and a shared worker.

@example

import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreWorker from './livestore.worker.ts?worker'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
const adapter = makePersistedAdapter({
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
storage: { type: 'opfs' },
})

makePersistedAdapter
({
storage: {
readonly type: "opfs";
readonly directory?: string | undefined;
}

Specifies where to persist data for this adapter

storage
: {
type: "opfs"
type
: 'opfs' },
worker: ((options: {
name: string;
}) => globalThis.Worker) | (new (options: {
name: string;
}) => globalThis.Worker)
worker
:
const LiveStoreWorker: new (options?: {
name?: string;
}) => Worker
LiveStoreWorker
,
sharedWorker: ((options: {
name: string;
}) => globalThis.SharedWorker) | (new (options: {
name: string;
}) => globalThis.SharedWorker)

This is mostly an implementation detail and needed to be exposed into app code due to a current Vite limitation (https://github.com/vitejs/vite/issues/8427).

In most cases this should look like:

import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
const adapter = makePersistedAdapter({
sharedWorker: LiveStoreSharedWorker,
// ...
})

sharedWorker
:
const LiveStoreSharedWorker: new (options?: {
name?: string;
}) => SharedWorker
LiveStoreSharedWorker
,
resetPersistence?: boolean

Warning: This will reset both the app and eventlog database. This should only be used during development.

@defaultfalse

resetPersistence
,
})

If you want to reset persistence manually, you can:

  1. Clear site data in Chrome DevTools (Application tab > Storage > Clear site data)
  2. Use console command if the above doesn’t work due to a Chrome OPFS bug:
const opfsRoot = await navigator.storage.getDirectory();
await opfsRoot.remove();

Note: Only use this during development while the app is running.

Assuming the web adapter in a multi-client, multi-tab browser application, a diagram looks like this:

  • The web adapter is using some browser APIs that might require a HTTPS connection (e.g. navigator.locks). It’s recommended to even use HTTPS during local development (e.g. via Caddy).
  • Notable required browser APIs: OPFS, SharedWorker, navigator.locks, WASM
  • The web adapter of LiveStore currently doesn’t work on Android browsers as they don’t support the SharedWorker API (see Chromium bug).
  • It’s recommended to develop in an incognito window to avoid issues with persistent storage (e.g. OPFS).

What’s the bundle size of the web adapter?

Section titled “What’s the bundle size of the web adapter?”

LiveStore with the web adapter adds two parts to your application bundle:

  • The LiveStore JavaScript bundle (~180KB gzipped)
  • SQLite WASM (~300KB gzipped)