{
try {
const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(JWT_SECRET))
return payload
} catch (error) {
console.log('⚠️ Error verifying token', error)
}
}
```
The above example uses [`jose`](https://www.npmjs.com/package/jose), a popular JavaScript module that supports JWTs. It works across various runtimes, including Node.js, Cloudflare Workers, Deno, Bun, and others.
The `validatePayload` function receives the `authToken`, checks if the payload exists, and verifies that it's valid and hasn't expired. If all checks pass, sync continues as normal. If any check fails, the server rejects the sync.
The client app still works as expected, but saves data locally. If the user re-authenticates or refreshes the token later, LiveStore syncs any local changes made while the user was unauthenticated.
# [Version control](https://dev.docs.livestore.dev/patterns/version-control/)
## Overview
LiveStore's event sourcing approach allows you to implement version control functionality in your application (similar to Git but for your application domain). This could include features like:
- Branching
- Semantic commit messages & grouping
- History tracking
- Semantic/interactive merges
# [Concepts](https://dev.docs.livestore.dev/reference/concepts/)
## Overview

## Overview
- Adapter (platform adapter)
- An adapter can instantiate a client session for a given platform (e.g. web, Expo)
- Client
- A logical group of client sessions
- Client session
- Store
- Reactivity graph
- Responsible for leader election
- [Devtools](/reference/devtools)
- [Events](/reference/events)
- Event definition
- Eventlog
- Synced vs client-only events
- Framework integration
- A framework integration is a package that provides a way to integrate LiveStore with a framework (e.g. React, Solid, Svelte, etc.)
- [Reactivity system](/reference/reactivity-system)
- Db queries `queryDb()`
- Computed queries `computed()`
- Signals `signal()`
- Schema
- LiveStore uses schema definitions for the following cases:
- [Event definitions](/reference/events)
- [SQLite state schema](/reference/state/sqlite-schema)
- [Query result schemas](/reference/state/sql-queries)
- LiveStore uses the [Effect Schema module](/patterns/effect) to define fine-granular schemas
- State
- Derived from the eventlog via materializers
- Materializer
- Event handler function that maps an event to a state change
- SQLite state / database
- In-memory SQLite database within the client session thread (usually main thread)
- Used by the reactivity graph
- Persisted SQLite database (usually running on the leader thread)
- Fully derived from the eventlog
- [Store](/reference/store)
- A store exposes most of LiveStore's functionality to the application layer and is the main entry point for using LiveStore.
- To create a store you need to provide a schema and a platform adapter which creates a client session.
- A store is often created, managed and accessed through a framework integration (like React).
- A is identified by a `storeId` which is also used for syncing events between clients.
- Sync provider
- A sync provider is a package that provides a sync backend and a sync client.
- Sync backend
- A central server that is responsible for syncing the eventlog between clients
### Implementation details
- Leader thread
- Responsible for syncing and persisting of data
- Sync processor
- LeaderSyncProcessor
- ClientSessionSyncProcessor
## Pluggable architecture
LiveStore is designed to be pluggable in various ways:
- Platform adapters
- Sync providers
- Framework integrations
# [Devtools](https://dev.docs.livestore.dev/reference/devtools/)
## Overview
NOTE: Once LiveStore is open source, the devtools will be a [sponsor-only benefit](/misc/sponsoring).
## Features
- Real-time data browser with 2-way sync

- Query inspector

- Eventlog browser

- Sync status

- Export/import

- Reactivity graph / signals inspector

- SQLite playground

## Adapters
### `@livestore/adapter-web`:
Requires the `@livestore/devtools-vite` package to be installed and configured in your Vite config:
```ts
// vite.config.js
import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite'
export default defineConfig({
// ...
plugins: [
livestoreDevtoolsPlugin({ schemaPath: './src/livestore/schema.ts' }),
],
})
```
The devtools can be opened in a separate tab (via e.g. `localhost:3000/_livestore/web). You should see the Devtools URL logged in the browser console when running the app.
#### Chrome extension
You can also use the Devtools Chrome extension.

Please make sure to manually install the extension version matching the LiveStore version you are using by downloading the appropriate version from the [GitHub releases page](https://github.com/livestorejs/livestore/releases) and installing it manually via `chrome://extensions/`.
To install the extension:
1. **Unpack the ZIP file** (e.g. `livestore-devtools-chrome-0.3.0.zip`) into a folder on your computer.
2. Navigate to `chrome://extensions/` and enable **Developer mode** (toggle in the top-right corner).
3. Click **"Load unpacked"** and select the unpacked folder or drag and drop the folder onto the page.
### `@livestore/adapter-expo`:
Requires the `@livestore/devtools-expo` package to be installed and configured in your metro config:
```ts
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config')
const { addLiveStoreDevtoolsMiddleware } = require('@livestore/devtools-expo')
const config = getDefaultConfig(__dirname)
addLiveStoreDevtoolsMiddleware(config, { schemaPath: './src/livestore/schema.ts' })
module.exports = config
```
You can open the devtools by pressing `Shift+m` in the Expo CLI process and then selecting `@livestore/devtools-expo` which will open the devtools in a new tab.
### `@livestore/adapter-node`:
Devtools are configured out of the box for the `makePersistedAdapter` variant (note currently not supported for the `makeInMemoryAdapter` variant).
You should see the Devtools URL logged when running the app.
# [Debugging a LiveStore app](https://dev.docs.livestore.dev/reference/debugging/)
## Overview
When working on a LiveStore app you might end up in situations where you need to debug things. LiveStore is built with debuggability in mind and tries to make your life as a developer as easy as possible.
Here are a few things that LiveStore offers to help you debug your app:
- [OpenTelemetry](/reference/opentelemetry) integration for tracing / metrics
- [Devtools](/reference/devtools) for inspecting the state of the store
- Store helper methods
## Debugging helpers on the store
The `store` exposes a `_dev` property which contains a few helpers that can help you debug your app.
## Other recommended practices and tools
- Use the step debugger
# [Events](https://dev.docs.livestore.dev/reference/events/)
## Overview
## Event definitions
There are two types of events:
- `synced`: Events that are synced across clients
- `clientOnly`: Events that are only processed locally on the client (but still synced across client sessions e.g. across browser tabs/windows)
An event definition consists of a unique name of the event and a schema for the event arguments. It's recommended to version event definitions to make it easier to evolve them over time.
Events will be synced across clients and materialized into state (i.e. SQLite tables) via [materializers](/reference/state/materializers).
### Example
```ts
// livestore/schema.ts
import { Events, Schema, sql } from '@livestore/livestore'
export const events = {
todoCreated: Events.synced({
name: 'v1.TodoCreated',
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
}),
todoCompleted: Events.synced({
name: 'v1.TodoCompleted',
schema: Schema.Struct({ id: Schema.String }),
}),
}
```
### Best Practices
- It's strongly recommended to use past-tense event names (e.g. `todoCreated`/`createdTodo` instead of `todoCreate`/`createTodo`) to indicate something already occurred.
- TODO: write down more best practices
- TODO: mention AI linting (either manually or via a CI step)
- core idea: feed list of best practices to AI and check if events adhere to them + get suggestions if not
- It's recommended to avoid `DELETE` events and instead use soft-deletes (e.g. add a `deleted` date/boolean column with a default value of `null`). This helps avoid some common concurrency issues.
### Schema evolution \{#schema-evolution\}
- Event definitions can't be removed after they were added to your app.
- Event schema definitions can be evolved as long as the changes are forward-compatible.
- That means data encoded with the old schema can be decoded with the new schema.
- In practice, this means ...
- for structs ...
- you can add new fields if they have default values or are optional
- you can remove fields
## Commiting events
```ts
// somewhere in your app
import { events } from './livestore/schema.js'
store.commit(
events.todoCreated({ id: '1', text: 'Buy milk' })
)
```
## Eventlog
The history of all events that have been committed is stored forms the "eventlog". It is persisted in the client as well as in the sync backend.
Example `eventlog.db`:

# [OpenTelemetry](https://dev.docs.livestore.dev/reference/opentelemetry/)
## Overview
LiveStore has built-in support for OpenTelemetry.
## Usage with React
```tsx
// otel.ts
const makeTracer = () => {
const url = import.meta.env.VITE_OTEL_EXPORTER_OTLP_ENDPOINT
const provider = new WebTracerProvider({
spanProcessors: [new SimpleSpanProcessor(new OTLPTraceExporter({ url }))],
})
provider.register()
return provider.getTracer('livestore')
}
export const tracer = makeTracer()
// In your main entry file
import { tracer } from './otel.js'
export const App: React.FC = () => (
)
// And in your `livestore.worker.ts`
import { tracer } from './otel.js'
makeWorker({ schema, otelOptions: { tracer } })
```
# [Store](https://dev.docs.livestore.dev/reference/store/)
## Overview
The `Store` is the most common way to interact with LiveStore from your application code. It provides a way to query data, commit events, and subscribe to data changes.
## Creating a store
For how to create a store in React, see the [React integration docs](/reference/framework-integrations/react-integration). The following example shows how to create a store manually:
```ts
import { createStorePromise } from '@livestore/livestore'
import { schema } from './livestore/schema.js'
const adapter = // ...
const store = await createStorePromise({
schema,
adapter,
storeId: 'some-store-id',
})
```
## Using a store
### Querying data
```ts
const todos = store.query(tables.todos)
```
### Subscribing to data
```ts
const unsubscribe = store.subscribe(tables.todos, (todos) => {
console.log(todos)
})
```
### Committing events
```ts
store.commit(events.todoCreated({ id: '1', text: 'Buy milk' }))
```
### Shutting down a store
```ts
await store.shutdown()
```
## Multiple Stores
You can create and use multiple stores in the same app. This can be useful when breaking up your data model into smaller pieces.
## Development/debugging helpers
A store instance also exposes a `_dev` property that contains some helpful methods for development. For convenience you can access a store on `globalThis`/`window` like via `__debugLiveStore.default._dev` (`default` is the store id):
```ts
// Download the SQLite database
__debugLiveStore.default._dev.downloadDb()
// Download the eventlog database
__debugLiveStore.default._dev.downloadEventlogDb()
// Reset the store
__debugLiveStore.default._dev.hardReset()
// See the current sync state
__debugLiveStore.default._dev.syncStates()
```
# [Reactivity System](https://dev.docs.livestore.dev/reference/reactivity-system/)
## Overview
LiveStore has a high-performance, fine-grained reactivity system built in which is similar to Signals (e.g. in [SolidJS](https://docs.solidjs.com/concepts/signals)).
## Defining reactive state
LiveStore provides 3 types of reactive state:
- Reactive SQL queries on top of SQLite state (`queryDb()`)
- Reactive state values (`signal()`)
- Reactive computed values (`computed()`)
Reactive state variables end on a `$` by convention (e.g. `todos$`). The `label` option is optional but can be used to identify the reactive state variable in the devtools.
### Reactive SQL queries
```ts
import { queryDb } from '@livestore/livestore'
const todos$ = queryDb(tables.todos.orderBy('createdAt', 'desc'), { label: 'todos$' })
// Or using callback syntax to depend on other queries
const todos$ = queryDb((get) => {
const { showCompleted } = get(uiState$)
return tables.todos.where(showCompleted ? { completed: true } : {})
}, { label: 'todos$' })
```
### Signals
Signals are reactive state values that can be set and get. This can be useful for state that is not materialized from events into SQLite tables.
```ts
import { signal } from '@livestore/livestore'
const now$ = signal(Date.now(), { label: 'now$' })
setInterval(() => {
store.setSignal(now$, Date.now())
}, 1000)
// Counter example
const num$ = signal(0, { label: 'num$' })
const increment = () => store.setSignal(num$, (prev) => prev + 1)
increment()
increment()
console.log(store.query(num$)) // 2
```
### Computed values
```ts
import { computed } from '@livestore/livestore'
const num$ = signal(0, { label: 'num$' })
const duplicated$ = computed((get) => get(num$) * 2, { label: 'duplicated$' })
```
## Accessing reactive state
Reactive state is always bound to a `Store` instance. You can access the current value of reactive state the following ways:
### Using the `Store` instance
```ts
// One-off query
const count = store.query(count$)
// By subscribing to the reactive state value
const unsub = count$.subscribe((count) => {
console.log(count)
})
```
### Via framework integrations
#### React
```ts
import { useQuery } from '@livestore/react'
const MyComponent = () => {
const value = useQuery(state$)
return {value}
}
```
#### Solid
```ts
import { query } from '@livestore/solid'
const MyComponent = () => {
const value = query(state$)
return {value}
}
```
### Reacting to changing variables passed to queries
If your query depends on a variable passed in by the component, use the deps array to react to changes in this variable.
```ts
const todos$ = ({ showCompleted } ) => queryDb((get) => {
return tables.todos.where(showCompleted ? { completed: true } : {})
}, {
label: 'todos$',
deps: [showCompleted] // 👈 add to deps array
})
const MyComponent = ({ showCompleted }) => {
const todos = store.useQuery(todos$({ showCompleted }))
return {todos.length} Done
}
```
## Further reading
- [Riffle](https://riffle.systems/essays/prelude/): Building data-centric apps with a reactive relational database
- [Adapton](http://adapton.org/) / [miniAdapton](https://arxiv.org/pdf/1609.05337)
## Related technologies
- [Signia](https://signia.tldraw.dev/): Signia is a minimal, fast, and scalable signals library for TypeScript developed by TLDraw.
# [Custom Elements](https://dev.docs.livestore.dev/reference/framework-integrations/custom-elements/)
## Overview
import { Code } from '@astrojs/starlight/components';
import customElementsCode from '../../../../../../examples/standalone/web-todomvc-custom-elements/src/main.ts?raw'
LiveStore can be used with custom elements/web components.
## Example
See [examples](/examples) for a complete example.
# [React integration for LiveStore](https://dev.docs.livestore.dev/reference/framework-integrations/react-integration/)
## Overview
While LiveStore is framework agnostic, the `@livestore/react` package provides a first-class integration with React.
## Features
- High performance
- Fine-grained reactivity (using LiveStore's signals-based reactivity system)
- Instant, synchronous query results (without the need for `useEffect` and `isLoading` checks)
- Transactional state transitions (via `batchUpdates`)
- Also supports Expo / React Native via `@livestore/adapter-expo`
## API
### `LiveStoreProvider`
In order to use LiveStore with React, you need to wrap your application in a `LiveStoreProvider`.
```tsx
import { LiveStoreProvider } from '@livestore/react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
const Root = () => {
return (
)
}
```
For scenarios where you have an existing store instance, you can manually create a `LiveStoreContext.Provider`:
```tsx
import { LiveStoreContext } from '@livestore/react'
import { createStorePromise } from '@livestore/livestore'
const store = createStorePromise({ /* ... */ })
const Root = () => {
return (
)
}
```
### useStore
```tsx
import { useStore } from '@livestore/react'
const MyComponent = () => {
const { store } = useStore()
React.useEffect(() => {
store.commit(tables.todos.insert({ id: '1', text: 'Hello, world!' }))
}, [])
return ...
}
```
### useQuery
```tsx
import { useStore } from '@livestore/react'
const query$ = tables.todos.query.where({ completed: true }).orderBy('createdAt', 'desc')
const CompletedTodos = () => {
const { store } = useStore()
const todos = store.useQuery(query$)
return {todos.map((todo) =>
{todo.text}
)}
}
```
### useClientDocument
```tsx
import { useStore } from '@livestore/react'
const TodoItem = ({ id }: { id: string }) => {
const { store } = useStore()
const [todo, updateTodo] = store.useClientDocument(tables.todos, id)
return updateTodo({ text: 'Hello, world!' })}>{todo.text}
}
```
## Usage with ...
### Vite
LiveStore works with Vite out of the box.
### Tanstack Start
LiveStore works with Tanstack Start out of the box.
### Expo / React Native
LiveStore has a first-class integration with Expo / React Native via `@livestore/adapter-expo`.
### Next.js
Given various Next.js limitations, LiveStore doesn't yet work with Next.js out of the box.
## Technical notes
- `@livestore/react` uses `React.useState` under the hood for `useQuery` / `useClientDocument` to bind LiveStore's reactivity to React's reactivity. Some libraries are using `React.useExternalSyncStore` for similar purposes but using `React.useState` in this case is more efficient and all that's needed for LiveStore.
- `@livestore/react` supports React Strict Mode.
# [Solid integration](https://dev.docs.livestore.dev/reference/framework-integrations/solid-integration/)
## Overview
import { Code } from '@astrojs/starlight/components';
import solidStoreCode from '../../../../../../examples/standalone/web-todomvc-solid/src/livestore/store.tsx?raw'
import solidMainSectionCode from '../../../../../../examples/standalone/web-todomvc-solid/src/components/MainSection.tsx?raw'
## Example
See [examples](/examples) for a complete example.
# [Electron Adapter](https://dev.docs.livestore.dev/reference/platform-adapters/electron-adapter/)
## Overview
LiveStore doesn't yet support Electron (see [this issue](https://github.com/livestorejs/livestore/issues/296) for more details).
# [Tauri Adapter](https://dev.docs.livestore.dev/reference/platform-adapters/tauri-adapter/)
## Overview
LiveStore doesn't yet support Tauri (see [this issue](https://github.com/livestorejs/livestore/issues/125) for more details).
# [SQLite State Schema](https://dev.docs.livestore.dev/reference/state/sqlite-schema/)
## Overview
import { Code, Tabs, TabItem } from '@astrojs/starlight/components';
import schemaCode from '../../../../../../examples/standalone/web-todomvc/src/livestore/schema.ts?raw'
LiveStore provides a schema definition language for defining your database tables and mutation definitions. LiveStore automatically migrates your database schema when you change your schema definitions.
### Example
### Schema migrations
Migration strategies:
- `auto`: Automatically migrate the database to the newest schema and rematerializes the state from the eventlog.
- `manual`: Manually migrate the database to the newest schema.
### Client documents
- Meant for convenience
- Client-only
- Goal: Similar ease of use as `React.useState`
- When schema changes in a non-backwards compatible way, previous events are dropped and the state is reset
- Don't use client documents for sensitive data which must not be lost
- Implies
- Table with `id` and `value` columns
- `${MyTable}Set` event + materializer (which are auto-registered)
### Column types
#### Core SQLite column types
- `State.SQLite.text`: A text field, returns `string`.
- `State.SQLite.integer`: An integer field, returns `number`.
- `State.SQLite.real`: A real field (floating point number), returns `number`.
- `State.SQLite.blob`: A blob field (binary data), returns `Uint8Array`.
#### Higher level column types
- `State.SQLite.boolean`: An integer field that stores `0` for `false` and `1` for `true` and returns a `boolean`.
- `State.SQLite.json`: A text field that stores a stringified JSON object and returns a decoded JSON value.
- `State.SQLite.datetime`: A text field that stores dates as ISO 8601 strings and returns a `Date`.
- `State.SQLite.datetimeInteger`: A integer field that stores dates as the number of milliseconds since the epoch and returns a `Date`.
#### Custom column schemas
You can also provide a custom schema for a column which is used to automatically encode and decode the column value.
#### Example: JSON-encoded struct
```ts
import { State, Schema } from '@livestore/livestore'
export const UserMetadata = Schema.Struct({
petName: Schema.String,
favoriteColor: Schema.Literal('red', 'blue', 'green'),
})
export const userTable = State.SQLite.table({
name: 'user',
columns: {
id: State.SQLite.text({ primaryKey: true }),
name: State.SQLite.text(),
metadata: State.SQLite.json({ schema: UserMetadata }),
}
})
```
## Best Practices
- It's usually recommend to **not distinguish** between app state vs app data but rather keep all state in LiveStore.
- This means you'll rarely use `React.useState` when using LiveStore
- In some cases for "fast changing values" it can make sense to keep a version of a state value outside of LiveStore with a reactive setter for React and a debounced setter for LiveStore to avoid excessive LiveStore mutations. Cases where this can make sense can include:
- Text input / rich text editing
- Scroll position tracking, resize events, move/drag events
- ...
# [SQLite in LiveStore](https://dev.docs.livestore.dev/reference/state/sqlite/)
## Overview
LiveStore heavily uses SQLite as its default state/read model.
## Implementation notes
- LiveStore relies on the following SQLite extensions to be available: `-DSQLITE_ENABLE_BYTECODE_VTAB -DSQLITE_ENABLE_SESSION -DSQLITE_ENABLE_PREUPDATE_HOOK`
- [bytecode](https://www.sqlite.org/bytecodevtab.html)
- [session](https://www.sqlite.org/sessionintro.html) (incl. preupdate)
- For web / node adapater:
- LiveStore uses [a fork](https://github.com/livestorejs/wa-sqlite) of the [wa-sqlite](https://github.com/rhashimoto/wa-sqlite) SQLite WASM library.
- In the future LiveStore might use a non-WASM build for Node/Bun/Deno/etc.
- For Expo adapter:
- LiveStore uses the official expo-sqlite library which supports LiveStore's SQLite requirements.
- LiveStore uses the `session` extension to enable efficient database rollback which is needed when the eventlog is rolled back as part of a rebase. An alternative implementation strategy would be to rely on snapshotting (i.e. periodically create database snapshots and roll back to the latest snapshot + applied missing mutations).
# [Syncing](https://dev.docs.livestore.dev/reference/syncing//)
## Overview
## How it works
LiveStore is based on [the idea of event-sourcing](/evaluation/event-sourcing) which means it syncs events across clients (via a central sync backend) and then materializes the events in the local SQLite database. This means LiveStore isn't syncing the SQLite database itself directly but only the events that are used to materialize the database making sure it's kept in sync across clients.
The syncing mechanism is similar to how Git works in that regard that it's based on a "push/pull" model. Upstream events always need to be pulled before a client can push its own events to preserve a [global total order of events](https://medium.com/baseds/ordering-distributed-events-29c1dd9d1eff). Local pending events which haven't been pushed yet need to be rebased on top of the latest upstream events before they can be pushed.
## Events
A LiveStore event consists of the following data:
- `seqNum`: event sequence number
- `parentSeqNum`: parent event sequence number
- `name`: event name (refers to a event definition in the schema)
- `args`: event arguments (encoded using the event's schema definition, usually JSON)
### Event sequence numbers
- Event sequence numbers: monotonically increasing integers
- client event sequence number to sync across client sessions (never exposed to the sync backend)
### Sync heads
- The latest event in a eventlog is referred to as the "head" (similar to how Git refers to the latest commit as the "head").
- Given that LiveStore does hierarchical syncing between the client session, the client leader and the sync backend, there are three heads (i.e. the client session head, the client leader head, and the sync backend head).
## Sync backend
The sync backend acts as the global authority and determines the total order of events ("causality"). It's responsible for storing and querying events and for notifying clients when new events are available.
### Requirements for sync backend
- Needs to provide an efficient way to query an ordered list of events given a starting event ID (often referred to as cursor).
- Ideally provides a "reactivity" mechanism to notify clients when new events are available (e.g. via WebSocket, HTTP long-polling, etc).
- Alternatively, the client can periodically query for new events which is less efficient.
## Clients
- Each client initialy chooses a random `clientId` as its globally unique ID
- LiveStore uses a 6-char nanoid
- In the unlikely event of a collision which is detected by the sync backend the first time a client tries to push, the client chooses a new random `clientId`, patches the local events with the new `clientId`, and tries again.
### Local syncing across client sessions
- For adapters which support multiple client sessions (e.g. web), LiveStore also supports local syncing across client sessions (e.g. across browser tabs or worker threads).
- LiveStore does this by electing a leader thread which is responsible for syncing and persiting data locally.
- Client session events are not synced to the sync backend.
## Auth (Authentication & Authorization)
- TODO
- Provide basic example
- Encryption
## Advanced
### Sequence diagrams
#### Pulling events (without unpushed events)
```mermaid
sequenceDiagram
participant Client
participant Sync Backend
Client->>Sync Backend: `pull` request (head_cursor)
Sync Backend->>Sync Backend: Get new events (since head_cursor)
Sync Backend-->>Client: New events
activate Client
Note over Client: Client is in sync
deactivate Client
```
#### Pushing events
```mermaid
sequenceDiagram
participant Client
participant Sync Backend
Client->>Client: Commits events
Client->>Sync Backend: `push` request (new_local_events)
activate Sync Backend
Sync Backend->>Sync Backend: Process push request (validate, persist)
Sync Backend-->>Client: Push Success
deactivate Sync Backend
Note over Client: Client is in sync
```
### Rebasing
### Merge conflicts
- Merge conflict handling isn't implemented yet (see [this issue](https://github.com/livestorejs/livestore/issues/253)).
- Merge conflict detection and resolution will be based on the upcoming [facts system functionality](https://github.com/livestorejs/livestore/issues/254).
### Compaction
- Compaction isn't implemented yet (see [this issue](https://github.com/livestorejs/livestore/issues/136))
- Compaction will be based on the upcoming [facts system functionality](https://github.com/livestorejs/livestore/issues/254).
### Partitioning
- Currently LiveStore assumes a 1:1 mapping between an eventlog and a SQLite database.
- In the future, LiveStore aims to support multiple eventlogs (see [this issue](https://github.com/livestorejs/livestore/issues/255)).
## Design decisions / trade-offs
- Require a central sync backend to enforce a global total order of events.
- This means LiveStore can't be used in a fully decentralized/P2P manner.
- Do rebasing on the client side (instead of on the sync backend). This allows the user to have more control over the rebase process.
## Notes
- Rich text data is best handled via CRDTs (see [#263](https://github.com/livestorejs/livestore/issues/263))
## Further reading
- Distributed Systems lecture series by Martin Kleppmann: [YouTube playlist](https://www.youtube.com/playlist?list=PLeKd45zvjcDFUEv_ohr_HdUFe97RItdiB) / [lecture notes](https://www.cl.cam.ac.uk/teaching/2122/ConcDisSys/dist-sys-notes.pdf)
# [Server-side clients](https://dev.docs.livestore.dev/reference/syncing/server-side-clients/)
## Overview
import { Code, Tabs, TabItem } from '@astrojs/starlight/components';
You can also use LiveStore on the server side e.g. via the `@livestore/adapter-node` adapter. This allows you to:
- have an up-to-date server-side SQLite database (read model)
- react to events / state changes on the server side (e.g. to send emails/push notifications)
- commit events on the server side (e.g. for sensitive/trusted operations)

Note about the schema: While the `events` schema needs to be shared across all clients, the `state` schema can be different for each client (e.g. to allow for a different SQLite table design on the server side).
## Example
## Further notes
### Cloudflare Workers
- The `@livestore/adapter-node` adapter doesn't yet work with Cloudflare Workers but you can follow [this issue](https://github.com/livestorejs/livestore/issues/266) for a Cloudflare adapter to enable this use case.
- Having a `@livestore/adapter-cf-worker` adapter could enable serverless server-side client scenarios.
# [Expo Adapter](https://dev.docs.livestore.dev/reference/platform-adapters/expo-adapter/)
## Overview
## Notes on Android
- By default, Android requires `https` (including WebSocket connections) when communicating with a sync backend.
To allow for `http` / `ws`, you can run `expo install expo-build-properties` and add the following to your `app.json` (see [here](https://docs.expo.dev/versions/latest/sdk/build-properties/#pluginconfigtypeandroid) for more information):
```json
{
"expo": {
"plugins": [
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
},
"ios": {}
}
]
}
}
```
# [Node Adapter](https://dev.docs.livestore.dev/reference/platform-adapters/node-adapter/)
## Overview
Works with Node.js, Bun and Deno.
## Example
```ts
import { makeAdapter } from '@livestore/adapter-node'
const adapter = makeAdapter({
storage: { type: 'fs' },
// or in-memory:
// storage: { type: 'in-memory' },
sync: { backend: makeCfSync({ url: 'ws://localhost:8787' }) },
// To enable devtools:
// devtools: { schemaPath: new URL('./schema.ts', import.meta.url) },
})
```
### Worker adapter
The worker adapter can be used for more advanced scenarios where it's preferable to reduce the load of the main thread and run persistence/syncing in a worker thread.
```ts
// main.ts
import { makeWorkerAdapter } from '@livestore/adapter-node'
const adapter = makeWorkerAdapter({
workerUrl: new URL('./livestore.worker.js', import.meta.url),
})
// livestore.worker.ts
import { makeWorker } from '@livestore/adapter-node/worker'
const adapter = makeAdapter({
storage: { type: 'fs' },
// or in-memory:
// storage: { type: 'in-memory' },
sync: { backend: makeCfSync({ url: 'ws://localhost:8787' }) },
})
```
# [SQL Queries](https://dev.docs.livestore.dev/reference/state/sql-queries/)
## Overview
## Query builder
LiveStore also provides a small query builder for the most common queries. The query builder automatically derives the appropriate result schema internally.
```ts
const table = State.SQLite.table({
name: 'my_table',
columns: {
id: State.SQLite.text({ primaryKey: true }),
name: State.SQLite.text(),
},
})
// Read queries
table.select('name')
table.where('name', '==', 'Alice')
table.where({ name: 'Alice' })
table.orderBy('name', 'desc').offset(10).limit(10)
table.count().where('name', 'like', '%Ali%')
// Write queries
table.insert({ id: '123', name: 'Bob' })
table.update({ name: 'Alice' }).where({ id: '123' })
table.delete().where({ id: '123' })
```
## Raw SQL queries
LiveStore supports arbitrary SQL queries on top of SQLite. In order for LiveStore to handle the query results correctly, you need to provide the result schema.
```ts
import { queryDb, State, Schema, sql } from '@livestore/livestore'
const table = State.SQLite.table({
name: 'my_table',
columns: {
id: State.SQLite.text({ primaryKey: true }),
name: State.SQLite.text(),
},
})
const filtered$ = queryDb({
query: sql`select * from my_table where name = 'Alice'`,
schema: Schema.Array(table.schema),
})
const count$ = queryDb({
query: sql`select count(*) as count from my_table`,
schema: Schema.Struct({ count: Schema.Number }).pipe(Schema.pluck('count'), Schema.Array, Schema.headOrElse()),
})
```
## Best Practices
- Query results should be treated as immutable/read-only
- For queries which could return many rows, it's recommended to paginate the results
- Usually both via paginated/virtualized rendering as well as paginated queries
- You'll get best query performance by using a `WHERE` clause over an indexed column combined with a `LIMIT` clause. Avoid `OFFSET` as it can be slow on large tables
- For very large/complex queries, it can also make sense to implement incremental view maintenance (IVM) for your queries
- You can for example do this by have a separate table which is a materialized version of your query results which you update manually (and ideally incrementally) as the underlying data changes.
# [Vue integration for LiveStore](https://dev.docs.livestore.dev/reference/framework-integrations/vue-integration/)
## Overview
The [vue-livestore](https://github.com/slashv/vue-livestore) package provides integration with Vue. It's currently in beta but aims to match feature parity with the React integration.
## API
### `LiveStoreProvider`
In order to use LiveStore with Vue, you need to wrap your application in a `LiveStoreProvider`.
```vue
Loading LiveStore...
```
### useStore
```ts
const { store } = useStore()
const createTodo = () => {
store.commit(
events.todoCreated({ id: crypto.randomUUID(), text: 'Eat broccoli' })
)
}
```
### useQuery
```vue
```
## Usage with ...
### Vite
LiveStore and vue-livestore works with Vite out of the box.
### Nuxt.js
Should work with Nuxt out of the box if SSR is disabled. It's on the road-map to figure out best way to approach integration. A good starting point for reference would be to look at [hello-zero-nuxt](https://github.com/danielroe/hello-zero-nuxt).
## Technical notes
- Vue-livestore uses the provider component pattern similar to the React integration. In Vue the plugin pattern is more common but it isn't clear that that's the most suitable structure for LiveStore in Vue. We might switch to the plugin pattern if we later find that more suitable especially with regards to Nuxt support and supporting multiple stores.
# [Materializers](https://dev.docs.livestore.dev/reference/state/materializers/)
## Overview
Materializers are functions that allow you to write to your database in response to events. Materializers are executed in the order of the events in the eventlog.
## Example
```ts
const events = {
todoCreated: Events.synced({
name: 'todoCreated',
schema: Schema.Struct({ id: Schema.String, text: Schema.String, completed: Schema.Boolean.pipe(Schema.optional) }),
}),
userPreferencesUpdated: Events.synced({
name: 'userPreferencesUpdated',
schema: Schema.Struct({ userId: Schema.String, theme: Schema.String }),
}),
factoryResetApplied: Events.synced({
name: 'factoryResetApplied',
schema: Schema.Struct({ }),
}),
}
/**
* A materializer function receives two arguments:
* 1. `eventPayload`: The deserialized data of the event.
* 2. `context`: An object containing:
* - `query`: A function to execute read queries against the current state of the database within the transaction.
* - `db`: The raw database instance (e.g., a Drizzle instance for SQLite).
* - `event`: The full event object, including metadata like event ID and timestamp.
*/
const materializers = State.SQLite.materializers(events, {
// Example of a single database write
todoCreated: ({ id, text, completed }, ctx) => todos.insert({ id, text, completed: completed ?? false }),
// Materializers can also have no return if no database writes are needed for an event
userPreferencesUpdated: ({ userId, theme }, ctx) => {
console.log(`User ${userId} updated theme to ${theme}. Event ID: ${ctx.event.id}`);
// No database write in this materializer
},
// It's also possible to return multiple database writes as an array
factoryResetApplied: (_payload, ctx) => [
table1.update({ someVal: 0 }),
table2.update({ otherVal: 'default' }),
// ...
]
}
```
## Reading from the database in materializers
Sometimes it can be useful to query your current state when executing a materializer. This can be done by using `ctx.query` in your materializer function.
```ts
const materializers = State.SQLite.materializers(events, {
todoCreated: ({ id, text, completed }, ctx) => {
const previousIds = ctx.query(todos.select('id'))
return todos.insert({ id, text, completed: completed ?? false, previousIds })
},
}
```
## Transactional behaviour
A materializer is always executed in a transaction. This transaction applies to:
- All database write operations returned by the materializer.
- Any `ctx.query` calls made within the materializer, ensuring a consistent view of the data.
Materializers can return:
- A single database write operation.
- An array of database write operations.
- `void` (i.e., no return value) if no database modifications are needed.
- An `Effect` that resolves to one of the above (e.g., `Effect.succeed(writeOp)` or `Effect.void`).
The `context` object passed to each materializer provides `query` for database reads, `db` for direct database access if needed, and `event` for the full event details.
## Error Handling
If a materializer function throws an error, or if an `Effect` returned by a materializer fails, the entire transaction for that event will be rolled back. This means any database changes attempted by that materializer for the failing event will not be persisted. The error will be logged, and the system will typically halt or flag the event as problematic, depending on the specific LiveStore setup.
If the error happens on the client which tries to commit the event, the event will never be committed and pushed to the sync backend.
In the future there will be ways to configure the error-handling behaviour, e.g. to allow skipping an incoming event when a materializer fails in order to avoid the app getting stuck. However, skipping events might also lead to diverging state across clients and should be used with caution.
## Best practices
### Side-effect free / deterministic
It's strongly recommended to make sure your materializers are side-effect free and deterministic. This also implies passing in all necessary data via the event payload.
Example:
```ts
// Don't do this
const events = {
todoCreated: Events.synced({
name: "v1.TodoCreated",
schema: Schema.Struct({ text: Schema.String }),
}),
}
const materializers = State.SQLite.materializers(events, {
"v1.TodoCreated": ({ text }) =>
tables.todos.insert({ id: crypto.randomUUID(), text }),
// ^^^^^^^^^^^^^^^^^^^
// This is non-deterministic
})
store.commit(events.todoCreated({ text: 'Buy groceries' }))
// Instead do this
const events = {
todoCreated: Events.synced({
name: "v1.TodoCreated",
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
// ^^^^^^^^^^^^^^^^^
// Also include the id in the event payload
}),
}
const materializers = State.SQLite.materializers(events, {
"v1.TodoCreated": ({ id, text }) => tables.todos.insert({ id, text }),
})
store.commit(events.todoCreated({ id: crypto.randomUUID(), text: 'Buy groceries' }))
```
# [Web Adapter](https://dev.docs.livestore.dev/reference/platform-adapters/web-adapter/)
## Overview
## Example
```ts
// main.ts
import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import LiveStoreWorker from './livestore.worker?worker'
const adapter = makePersistedAdapter({
storage: { type: 'opfs' },
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
})
```
```ts
// livestore.worker.ts
import { makeWorker } from '@livestore/adapter-web/worker'
import { schema } from './schema/index.js'
makeWorker({ schema })
```
## Adding a sync backend
```ts
// livestore.worker.ts
import { makeSomeSyncBackend } from '@livestore/sync-some-sync-backend'
makeWorker({ schema, sync: { backend: makeSomeSyncBackend('...') } })
```
## In-memory adapter
You can also use the in-memory adapter which can be useful in certain scenarios (e.g. testing).
```ts
import { makeInMemoryAdapter } from '@livestore/adapter-web'
const adapter = makeInMemoryAdapter({
schema,
// sync: { backend: makeSomeSyncBackend('...') },
})
```
## Web worker
- 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:
```ts
import '@livestore/adapter-web/worker-vite-dev-polyfill'
```
### 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.)
### Why not use a service worker?
- 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.
## Storage
LiveStore currently only support OPFS to locally persist its data. In the future we might add support for other storage types (e.g. IndexedDB).
LiveStore also uses `window.sessionStorage` to retain the identity of a client session (e.g. tab/window) across reloads.
In case you want to reset the local persistence of a client, you can provide the `resetPersistence` option to the adapter.
```ts
// Example which resets the persistence when the URL contains a `reset` query param
const resetPersistence = import.meta.env.DEV && new URLSearchParams(window.location.search).get('reset') !== null
if (resetPersistence) {
const searchParams = new URLSearchParams(window.location.search)
searchParams.delete('reset')
window.history.replaceState(null, '', `${window.location.pathname}?${searchParams.toString()}`)
}
const adapter = makePersistedAdapter({
storage: { type: 'opfs' },
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
resetPersistence
})
```
## Architecture diagram
Assuming the web adapter in a multi-client, multi-tab browser application, a diagram looks like this:

## Other notes
- 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](https://caddyserver.com/docs/automatic-https)).
## Browser support
- 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](https://issues.chromium.org/issues/40290702)).
## Best Practices
- It's recommended to develop in an incognito window to avoid issues with persistent storage (e.g. OPFS).
## FAQ
### 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)
# [Cloudflare Workers](https://dev.docs.livestore.dev/reference/syncing/sync-provider/cloudflare/)
## Overview
The `@livestore/sync-cf` package provides a LiveStore sync provider targeting Cloudflare Workers using Durable Objects (for websocket connections) and D1 (for persisting events).
## Example
### Using the web adapter
In your `livestore.worker.ts` file, you can use the `makeCfSync` function to create a sync backend.
```ts
import { makeCfSync } from '@livestore/sync-cf'
import { makeWorker } from '@livestore/adapter-web/worker'
import { schema } from './livestore/schema.js'
const url = 'ws://localhost:8787'
// const url = 'https://websocket-server.your-user.workers.dev
makeWorker({
schema,
sync: { backend: makeCfSync({ url }) },
})
```
### Cloudflare Worker
In your CF worker file, you can use the `makeDurableObject` and `makeWorker` functions to create a sync backend.
```ts
import { makeDurableObject, makeWorker } from '@livestore/sync-cf/cf-worker'
export class WebSocketServer extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}
export default makeWorker({
validatePayload: (payload: any) => {
if (payload?.authToken !== 'insecure-token-change-me') {
throw new Error('Invalid auth token')
}
},
})
```
#### Custom Cloudflare Worker handling
If you want to embed the sync backend request handler in your own Cloudflare worker, you can do so by using the `handleWebSocket` function for the `/websocket` endpoint.
```ts
import { handleWebSocket } from '@livestore/sync-cf/cf-worker'
export default {
fetch: async (request: Request, env: Env, ctx: ExecutionContext) => {
const url = new URL(request.url)
if (url.pathname.endsWith('/websocket')) {
return handleWebSocket(request, env, ctx, {
validatePayload: (payload: any) => {
if (payload?.authToken !== 'insecure-token-change-me') {
throw new Error('Invalid auth token')
}
},
})
}
return new Response('Invalid path', { status: 400 })
},
}
```
## Deployment
The sync backend can be deployed to Cloudflare using the following command:
```bash
wrangler deploy
```
## How the sync backend works
- A Cloudflare worker is used to open a websocket connection between the client and a durable object.
- The durable object answers push/pull requests from the client.
- The events are stored in a D1 SQLite database with a table for each store instance following the pattern `eventlog_${PERSISTENCE_FORMAT_VERSION}_${storeId}` where `PERSISTENCE_FORMAT_VERSION` is a number that is incremented whenever the `sync-cf` internal storage format changes.
## Local development
You can run the sync backend locally by running `wrangler dev` (e.g. take a look at the `todomvc-sync-cf` example). The local D1 database can be found in `.wrangler/state/d1/miniflare-D1DatabaseObject/XXX.sqlite`.
# [Build your own sync provider](https://dev.docs.livestore.dev/reference/syncing/sync-provider/custom/)
## Overview
It's very straightforward to implement your own sync provider. A sync provider implementation needs to do the following:
## Client-side
Implement the `SyncBackend` interface (running in the client) which describes the protocol for syncing events between the client and the server.
```ts
// Slightly simplified API (see packages/@livestore/common/src/sync/sync.ts for the full API)
export type SyncBackend = {
pull: (cursor: EventSequenceNumber) => Stream<{ batch: LiveStoreEvent[] }, InvalidPullError>
push: (batch: LiveStoreEvent[]) => Effect
}
// my-sync-backend.ts
const makeMySyncBackend = (args: { /* ... */ }) => {
return {
pull: (cursor) => {
// ...
},
push: (batch) => {
// ...
}
}
}
// my-app.ts
const adapter = makeAdapter({
sync: {
backend: makeMySyncBackend({ /* ... */ })
}
})
```
The actual implementation of those methods is left to the developer and mostly depends on the network protocol used to communicate between the client and the server.
Ideally this implementation considers the following:
- Network connectivity (offline, unstable connection, etc.)
- Ordering of events in case of out-of-order delivery
- Backoff and retry logic
## Server-side
Implement the actual sync backend protocol (running in the server). At minimum this sync backend needs to do the following:
- For client `push` requests:
- Validate the batch of events
- Ensure the batch sequence numbers are in ascending order and larger than the sync backend head
- Further validation checks (e.g. schema-aware payload validation)
- Persist the events in the event store (implying a new sync backend head equal to the sequence number of the pushed last event)
- Return a success response
- It's important that the server only processes one push request at a time to ensure a total ordering of events.
- For client `pull` requests:
- Validate the cursor
- Query the events from the database
- Return the events to the client
- This can be done in a batch or streamed to the client
- `pull` requests can be handled in parallel by the server
## General recommendations
It's recommended to study the existing sync backend implementations for inspiration.
# [ElectricSQL](https://dev.docs.livestore.dev/reference/syncing/sync-provider/electricsql/)
## Overview
## Example
See the [todomvc-sync-electric](https://github.com/livestorejs/livestore/tree/main/examples/src/web-todomvc-sync-electric) example.
## How the sync provider works
The initial version of the ElectricSQL sync provider will use the server-side Postgres DB as a store for the mutation event history.
Events are stored in a table following the pattern `eventlog_${PERSISTENCE_FORMAT_VERSION}_${storeId}` where `PERSISTENCE_FORMAT_VERSION` is a number that is incremented whenever the `sync-electric` internal storage format changes.
## F.A.Q.
### Can I use my existing Postgres database with the sync provider?
Unless the database is already modelled as a eventlog following the `@livestore/sync-electric` storage format, you won't be able to easily use your existing database with this sync backend implementation.
We might support this use case in the future, you can follow the progress [here](https://github.com/livestorejs/livestore/issues/286). Please share any feedback you have on this use case there.
### Why do I need my own API endpoint in front of the ElectricSQL server?
The API endpoint is used to proxy pull/push requests to the ElectricSQL server in order to implement any custom logic you might need, e.g. auth, rate limiting, etc.
# [S2](https://dev.docs.livestore.dev/reference/syncing/sync-provider/s2/)
## Overview
Syncing provider for [S2](https://s2.dev/) is planned.