{
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.
# [Events](https://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
- 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://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 } })
```
# [Reactivity System](https://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}
}
```
## 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.
# [Store](https://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()
```
# [Custom Elements](https://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.
# [Solid integration](https://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.
# [React integration for LiveStore](https://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 (
)
}
```
### 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.
# [SQLite State Schema](https://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://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).
# [Electron Adapter](https://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://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).
# [Syncing](https://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://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://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://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://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://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://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://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)
# [Code of Conduct](https://docs.livestore.dev/misc/CODE_OF_CONDUCT/)
## Overview
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[contact@livestore.dev][Contact].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][Homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][Translations].
[Homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[Translations]: https://www.contributor-covenant.org/translations
[Contact]: mailto:contact@livestore.dev
# [Frequently Asked Questions](https://docs.livestore.dev/misc/FAQ/)
## Overview
### Does LiveStore have optimistic updates?
Yes and no. LiveStore doesn't have the concept of optimistic updates as you might know from libraries like [React Query](https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates), however, any data update in LiveStore is automatically optimistic without the developer having to implement any special logic.
This provides the benefits of optimistic updates without the extra complexity by manually having to implement the logic for each individual data update (which can be very error prone).
### Does LiveStore have database transactions?
LiveStore runs on the client-side and handles transactions differently than traditional server-side databases. While materializers automatically run in transactions, global transactional behavior (often called "online transactions") needs to be explicitly modeled in your application logic.
### Can I use an ORM or query builder with LiveStore?
It's possible to use most ORMs/query builders with LiveStore (as long as they are able to synchronously generate SQL statements). You should also give the built-in LiveStore query builder a try. See [the ORM page](/patterns/orm) for more information.
### Is there a company behind LiveStore? How does LiveStore make money?
LiveStore is developed by [Johannes Schickling](https://github.com/schickling) and has been incubated as the foundation of [Overtone](https://overtone.pro) (a local-first music app). The plan is to keep the development of LiveStore as sustainable as possible via sponsorships and other paths (e.g. commercial licenses, paid consulting, premium devtools, etc).
### Is there a hosted sync backend provided by LiveStore?
No, LiveStore is designed to be self-hosted or be used with a 3rd party sync backend.
### Can I use my existing database with LiveStore? {#existing-database}
Not currently. LiveStore is built around the idea of event-sourcing which separates reads and writes. This means LiveStore isn't syncing your database directly but only the events that are used to materialize the database making sure it's kept in sync across clients.
However, we might provide support for this in the future depending on demand.
# [Community](https://docs.livestore.dev/misc/community/)
## Overview
import { DISCORD_INVITE_URL } from '../../../../../CONSTANTS.js'
import { officeHours } from '../../../../data.js'
## Discord
You can join the Discord server here.
## Office hours
You can join future office hour events [here](https://lu.ma/livestore).
{
officeHours.map((url) => (
))
}
{/* TODO add conference talks */}
{/* ## Conference talks */}
## RFX: Request for exploration \{#rfx\}
LiveStore opens the door to many new possibilities. Many more than I could explore or build myself, so I invite you to explore some of the ideas below.
### Technological ideas
- Auth
- Authn
- Authz
- e2ee
- Server side
- React server rendering
- Centralized read models
- Integrating with existing databases / systems
- CRDTs for text editing
- Automerge / YJS as embedded data
- Collaboration
- Presence features
- Blob files
- Version control
- Manual push/pull + git-like commits of multiple events
- Event-sourcing
- Schema evolution: migrating events (e.g. cambria)
- Cross-app data interop
- AI
- Local RAG
- Agents
### Application ideas
It would be great to see a new generation of apps built with LiveStore - ideally each app being:
- [Local-first](https://www.inkandswitch.com/essay/local-first/)
- Open-source
- Self-hostable
Here are some app ideas:
- Replacement for Doodle
- Replacement for Canny (feature requests)
- Replacement for Splitwise
- Replacement for Wunderlist
- GitHub client
- A secret Santa app
- Movie / TV tracking app
- Fitness app
### LiveStore internals
- Explore and improve multi-store ergonomics
- Diff queries in SQLite
- IVM
# [Design Partners](https://docs.livestore.dev/misc/design-partners/)
## Overview
LiveStore is looking for design partners with the following aims:
- For your company:
- Architectural guidance and internal training
- Priority support
- Influence over the roadmap and prioritization of features/bugfixes
- Make sure LiveStore is well-maintained as a critical part of your product
- For LiveStore:
- Sustain the continous development and maintenance of the project
- Make sure LiveStore is a designed around real-world use cases and constraints
Please [get in touch](https://forms.gle/NUy9irooEpXjqFAb6) if you're interested in becoming a design partner.
# [Note on Package Management](https://docs.livestore.dev/misc/package-management/)
## Overview
export const catalog = `\
catalog:
effect: ${EFFECT_VERSION} # As LiveStore depends on \`effect\`
# also \`react\`, \`react-dom\` etc based on your project
`
import { EFFECT_VERSION } from '../../../../../CONSTANTS.js'
import { Code } from '@astrojs/starlight/components';
## Recommended
It's strongly recommended to use `pnpm` or `bun` when building an app with LiveStore to avoid dependency issues (e.g. wrong version resolution, duplicate dependencies, etc).
### Peer dependencies
Since LiveStore has a few peer dependencies, you either should manually add them to your project or add the `@livestore/peer-deps` package to your project to satisfy them.
### PNPM Catalog
When using `pnpm`, we recommend specifying the following packages in your [PNPM Catalog](https://pnpm.io/catalogs):
# [Resources](https://docs.livestore.dev/misc/resources/)
## Overview
Feel free to use the following assets for presentations, blog posts, etc about LiveStore.
## Logo
Dark PNG |
Dark SVG |
Light PNG |
Light SVG |
|
|
|
|
## Architecture Diagrams
### Client scenarios
### Sync architecture
### Data modeling
# [Sponsoring LiveStore](https://docs.livestore.dev/misc/sponsoring/)
## Overview
import { Sponsor } from '../../../components/Sponsor'
## TLDR
- Sponsoring LiveStore helps ensure long-term stability and improvements for the project you rely on.
- Sponsors receive exclusive benefits, including a LiveStore Devtools license and access to sponsor-only resources.
- LiveStore is fully open source and community-supported—your sponsorship directly enables its ongoing development.
## Goal: Sustainable Open Source
As the creator and maintainer of LiveStore, I'm often asked *"how do you make money with LiveStore?"*. That's a great question with a simple answer. I'm not building LiveStore to make a lot of money - my goal is to make LiveStore a sustainable open source project. I've been working on LiveStore since 2021 (mostly full-time) and hope you can help to keep the project sustainable.
Open source has been a big part of my life - I've founded [Prisma](https://www.prisma.io/), created [Contentlayer](https://www.contentlayer.dev/), and built/maintained many other open source projects over the years. Through these experiences, I've seen firsthand how challenging it can be to keep open source projects healthy in the long run. Too often, maintainers burn out, and projects that many people depend on end up dying. My goal with LiveStore is to build an open source project that's sustainable and could possibly serve as an inspiration for other open source projects.
I wanted LiveStore to exist for over a decade - something I felt was missing in the ecosystem and that I know others have wanted as well. But building and maintaining a project on that level of ambition is incredibly hard, especially without a clear path to monetization. I believe that's also why a technology like LiveStore didn't exist yet.
Particularly being concerned about the sustainability of open source projects, I was hesitant to start another open source project myself. Still, I believe deeply in the value LiveStore creates for developers, and I'm committed to making it the best it can be.
The unfortunate reality is that there is no well-established way for open source creators to get paid for their work. While there are some great initiatives and platforms out there - like [Open Source Pledge](https://opensourcepledge.org/), [Generous](https://generous.builders/), [GitHub Sponsors](https://github.com/sponsors), [Polar](https://polar.sh/), [OpenCollective](https://opencollective.com/), and [thanks.dev](https://thanks.dev/) - most open source projects still struggle to capture even a fraction of the value they create. I believe in a positive-sum world, and I'm happy to contribute, but sustainability is essential if LiveStore is going to keep growing and improving.
My mid-/long term goal is to bring in enough resources not just to support myself, but to pay others to work on LiveStore as well. I want to ensure that the project remains stable, well-maintained, and innovative - something you can truly rely on. Sponsorship is the most direct way to make this possible. It's not just about funding features or bug fixes; it's about creating a relationship where your support helps guarantee the future of a tool you depend on.
I hope those words resonate with you and you'll understand why sponsoring LiveStore isn't just a nice gesture - it's essential for keeping the project going and a direct investment in the stability and evolution of a project that your application (and business) depends on. Your support ensures that LiveStore remains sustainable and healthy for the whole community.
Thank you! 🧡
## Related posts
- [The Open Source Sustainability Crisis](https://openpath.quest/2024/the-open-source-sustainability-crisis/) by Chad Whitacre
- [Entitlement in Open Source](https://mikemcquaid.com/entitlement-in-open-source/) by Homebrew lead Mike McQuaid
## Aligned Incentives
- **Stability and Reliability:**
Your application depends on LiveStore as the core data foundation. Sponsorship ensures continuous, focused maintenance and improvement, directly benefiting you with increased stability, reliability, and performance.
- **Shared Investment in Long-term Success:**
Sponsorship creates mutual investment in the project's future. You sponsoring LiveStore signals that it is crucial to your business, motivating me (and other maintainers) to prioritize features, enhancements, and fixes that benefit you.
- **Avoiding Costly In-House Development:**
LiveStore offers a unique architectural design with (currently) no direct alternatives readily available. So the only alternative is building something similar in-house which is takes a lot of time and resources. Sponsorship aligns incentives by ensuring LiveStore remains an attractive and efficient alternative.
- **Transparent Sustainability:**
Open-source sustainability is a common challenge as projects stagnate without sustainable resources. Sponsorship transparently addresses this, providing maintainers with clarity and stability, ensuring the project thrives and evolves to users' benefit.
- **Healthy Community and Ecosystem**:
Sponsors actively contribute to fostering a healthy, collaborative ecosystem. Their direct involvement ensures responsiveness to real-world needs, creating an ongoing, beneficial dialogue between users and maintainers.
- **Focused Innovation and Quality**:
Regular sponsorship allows maintainers to allocate dedicated time to innovative research and high-quality development. This ultimately translates into more reliable software, fewer bugs, faster releases, and thoughtful features driven by actual user needs.
- **Ensuring Longevity and Avoiding Lock-In**:
By aligning financial incentives through sponsorship, maintainers avoid being forced into less favorable monetization methods (e.g., restrictive licensing or heavy commercial lock-ins), maintaining open access and flexibility for users.
## Sponsor Benefits
You can access your sponsor benefits via the [Sponsor dashboard](https://livestore.dev/sponsor).
- [LiveStore Devtools](/reference/devtools) License
- Access to
- Sponsor-only Discord channels
- LiveStore Office Hours
- Prioritized bug fixes and feature requests
## Thanks to our Sponsors
A big and heartfelt thank you to all our sponsors. Your support has been invaluable and LiveStore wouldn't be where it is without you.
### Partners
- [ElectricSQL](https://www.electricsql.com/)
- [Netlify](https://www.netlify.com/)
- [Expo](https://expo.dev/)
- [Axial](https://axial.work/)
### Individuals
A big thank you to all individual GitHub sponsors! 🧡
## FAQ
### Why not raise VC money for LiveStore?
While raising venture capital for LiveStore might be possible, the challenge lies in building a VC-scale business around LiveStore. My current goal is to make and keep LiveStore sustainable without investor funding. (While I don't rule out this path in the future, it's currently not planned.)
### Why not build a hosting service around LiveStore?
While technically feasible, LiveStore embraces partnerships with other syncing services to create a win-win situation and minimize vendor lock-in for users.
### Are free devtools licenses for students?
Yes, please reach out via Discord.
### Are there other ways to support LiveStore?
Yes, there are many ways to support LiveStore:
- Become a [contributor / maintainer](/contributing/contributing)
- Help other community members (e.g. via Discord)
- Spread the word
- Give talks, write blog posts, post on social media, ...
- Provide feedback (e.g. via GitHub issues or Discord)
# [Troubleshooting](https://docs.livestore.dev/misc/troubleshooting/)
## Overview
### Store / sync backend is stuck in a weird state
While hopefully rare in practice, it might still happen that a client or a sync backend is stuck in a weird/invalid state. Please report such cases as a [GitHub issue](https://github.com/livestorejs/livestore/issues).
To avoid being stuck, you can either:
- use a different `storeId`
- or reset the sync backend and local client for the given `storeId`
## React related issues
### Query doesn't update properly
If you notice the result of a `useQuery` hook is not updating properly, you might be missing some dependencies in the query's hash.
For example, the following query:
```ts
// Don't do this
const query$ = useQuery(queryDb(tables.issues.query.where({ id: issueId }).first()))
// ^^^^^^^ missing in deps
// Do this instead
const query$ = useQuery(queryDb(tables.issues.query.where({ id: issueId }).first(), { deps: [issueId] }))
```
## `node_modules` related issues
### `Cannot execute an Effect versioned ...`
If you're seeing an error like `RuntimeException: Cannot execute an Effect versioned 3.10.13 with a Runtime of version 3.10.12`, you likely have multiple versions of `effect` installed in your project.
As a first step you can try deleting `node_modules` and running `pnpm install` again.
If the issue persists, you can try to add `"resolutions": { "effect": "3.15.2" }` or [`pnpm.overrides`](https://pnpm.io/package_json#pnpmoverrides) to your `package.json` to force the correct version of `effect` to be used.
## Package management
- Please make sure you only have a single version of any given package in your project (incl. LiveStore and other packages like `react`, etc). Having multiple versions of the same package can lead to all kinds of issues and should be avoided. This is particularly important when using LiveStore in a monorepo.
- Setting `resolutions` in your root `package.json` or tools like [PNPM catalogs](https://pnpm.io/catalogs) or [Syncpack](https://github.com/JamieMason/syncpack) can help you manage this.
# [Credits](https://docs.livestore.dev/misc/credits/)
## Overview
LiveStore wouldn't have been possible without the help and support of many individuals and companies.
A special thanks goes to:
- Geoffrey Litt & Nicholas Schiefer for the collaboration of the [Riffle research project](https://riffle.systems/essays/prelude/) on which LiveStore is based on
- Matt Wonlaw for the collaboration on LiveStore over an extended period of time
- Ink & Switch for their visionary [local-first research](https://inkandswitch.com/local-first/) and for being a continuous source of inspiration
- The SQLite team for their amazing work on the SQLite core library
- Roy Hashimoto for their great work on the SQLite WASM library [wa-sqlite](https://github.com/rhashimoto/wa-sqlite) which LiveStore uses a fork of
- Tim Suchanek for the initial collaboration on the Effect DB schema library
- All sponsors, users & community members for feedback and support
# [Cloudflare Workers](https://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`.
# [ElectricSQL](https://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.
# [Build your own sync provider](https://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.
# [S2](https://docs.livestore.dev/reference/syncing/sync-provider/s2/)
## Overview
Syncing provider for [S2](https://s2.dev/) is planned.