/Documents/SQLite/app.db`
To open the database in Finder, run the following command in your terminal:
```bash
open $(xcrun simctl get_app_container booted [APP_BUNDLE_ID] data)/Documents/SQLite
```
Replace `[APP_BUNDLE_ID]` with your app's bundle ID. e.g. `dev.livestore.livestore-expo`.
## Further notes
- LiveStore doesn't yet support Expo Web (see [#130](https://github.com/livestorejs/livestore/issues/130))
# [Node](https://livestore.dev/docs/getting-started/node/)
## Overview
TODO
# [React Web](https://livestore.dev/docs/getting-started/react-web/)
## Overview
import { Steps, Tabs, TabItem, Code } from '@astrojs/starlight/components';
import viteConfigCode from '../../../../../../examples/standalone/web-todomvc/vite.config.js?raw'
import eventsCode from '../../../../../../examples/standalone/web-todomvc/src/livestore/events.ts?raw'
import schemaCode from '../../../../../../examples/standalone/web-todomvc/src/livestore/schema.ts?raw'
import workerCode from '../../../../../../examples/standalone/web-todomvc/src/livestore/livestore.worker.ts?raw'
import rootCode from '../../../../../../examples/standalone/web-todomvc/src/Root.tsx?raw'
import headerCode from '../../../../../../examples/standalone/web-todomvc/src/components/Header.tsx?raw'
import mainSectionCode from '../../../../../../examples/standalone/web-todomvc/src/components/MainSection.tsx?raw'
export const CODE = {
viteConfig: viteConfigCode,
events: eventsCode,
schema: schemaCode,
worker: workerCode,
root: rootCode,
header: headerCode,
mainSection: mainSectionCode,
}
### Option A: Quick start
For a quick start, we recommend using our template app following the steps below.
For existing projects, see [Existing project setup](#existing-project-setup).
1. **Set up project from template**
```shell
bunx tiged --mode=git git@github.com:livestorejs/livestore/examples/standalone/web-linearlite my-app
```
Replace `my-app` with your desired app name.
2. **Install dependencies**
It's strongly recommended to use `bun` or `pnpm` for the simplest and most reliable dependency setup (see [note on package management](/docs/misc/package-management) for more details).
```bash
bun install
```
```bash
pnpm install
```
```bash
npm install
```
Pro tip: You can use [direnv](https://direnv.net/) to manage environment variables.
3. **Run dev environment**
```shell
bun dev
```
```shell
pnpm dev
```
```shell
npm run dev
```
4. **Open browser**
Open `http://localhost:60000` in your browser.
You can also open the devtools by going to `http://localhost:60000/_livestore`.
### Option B: Existing project setup \{#existing-project-setup\}
1. **Install dependencies**
```shell
bun add @livestore/livestore @livestore/wa-sqlite @livestore/adapter-web @livestore/react @livestore/utils @livestore/peer-deps @livestore/devtools-vite
```
```shell
pnpm add @livestore/livestore @livestore/wa-sqlite @livestore/adapter-web @livestore/react @livestore/utils @livestore/peer-deps @livestore/devtools-vite
```
```shell
npm install @livestore/livestore @livestore/wa-sqlite @livestore/adapter-web @livestore/react @livestore/utils @livestore/peer-deps @livestore/devtools-vite
```
2. **Update Vite config**
Add the following code to your `vite.config.js` file:
## Events
Create a file named `events.ts` inside the `livestore` folder. This file stores the events your app uses to interact with the database.
Use the `Events` and `Schema` modules from `@livestore/livestore` to define your events.
Here's an example:
## Define your schema
To define the data structure for your app, set up a schema that specifies the tables and fields your app uses.
- In `src`, create a `livestore` folder and inside it create a file named `schema.ts`. This file defines the tables and data structures for your app.
- In `schema.ts`, define a table to represent a data model, such as a `todos`.
Here's an example:
## Create the LiveStore Worker
Create a file named `livestore.worker.ts` inside the `src/livestore` folder. This file will contain the LiveStore web worker. When importing this file, make sure to add the `?worker` extension to the import path to ensure that Vite treats it as a worker file.
## Add the LiveStore Provider
To make the LiveStore available throughout your app, wrap your app's root component with the `LiveStoreProvider` component from `@livestore/react`. This provider manages your app’s data store, loading, and error states.
Here's an example:
### Commit events
After wrapping your app with the `LiveStoreProvider`, you can use the `useStore` hook from any component to commit events.
Here's an example:
## Queries
To retrieve data from the database, first define a query using `queryDb` from `@livestore/livestore`. Then, execute the query with the `useQuery` hook from `@livestore/react`.
Consider abstracting queries into a separate file to keep your code organized, though you can also define them directly within components if preferred.
Here's an example:
# [Code of Conduct](https://livestore.dev/docs/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://livestore.dev/docs/misc/FAQ/)
## Overview
## 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.
## 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}
# [Community](https://livestore.dev/docs/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
{
officeHours.map((url) => (
))
}
## 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
- Diff queries in SQLite
- IVM
# [Credits](https://livestore.dev/docs/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
# [Note on Package Management](https://livestore.dev/docs/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):
# [Sponsoring](https://livestore.dev/docs/misc/sponsoring/)
## Overview
LiveStore is currently only available for [GitHub Sponsors](https://github.com/sponsors/schickling).
## Thanks to our Sponsors
### Partners
- [ElectricSQL](https://www.electricsql.com/)
- [Netlify](https://www.netlify.com/)
### Individuals
A big thank you to all individual GitHub sponsors!
# [Anonymous user transition](https://livestore.dev/docs/patterns/anonymous-user-transition/)
## Overview
## Basic idea
- Locally choose a unique identifier for the user (e.g. via `crypto.randomUUID()`).
- You might want to handle the very unlikely case that the identifier is not unique (collision) on the sync backend.
- Persist this identifier locally (either via a separate LiveStore instance or via `localStorage`).
- Use this identifier in the `storeId` for the user-related LiveStore instance.
- Initially when the user is anonymous, the store won't be synced yet (i.e. no sync backend used in adapter).
- As part of the auth flow, the LiveStore instance is now synced with the same `storeId` to a sync backend which will sync all local events to the sync backend making sure the user keeps all their data.
# [Auth](https://livestore.dev/docs/patterns/auth/)
## Overview
LiveStore doesn't yet have any built-in authentication/authorization support, however it's possible to implement your own in the application layer.
## Passing auth payload to sync backend
You can use the `syncPayload` store option to pass an arbitrary payload to the sync backend.
# [Encryption](https://livestore.dev/docs/patterns/encryption/)
## Overview
LiveStore doesn't yet support encryption but might in the future.
See [this issue](https://github.com/livestorejs/livestore/issues/70) for more details.
For now you can implement encryption yourself e.g. by encrypting the events using a custom Effect Schema definition which applies a encryption transformation to the events.
# [Presence](https://livestore.dev/docs/patterns/presence/)
## Overview
LiveStore doesn't yet have any built-in presence functionality (e.g. to track online/offline users).
Common presence use cases are:
- Track which users are online / in a room
- Track which users are typing (e.g. in a chat)
- Text cursor (similar to Google Docs)
- Cursor movements (similar to Figma)
For now it's recommend to implement presence functionality in your application or use a third party service (e.g. Liveblocks).
# [Concepts](https://livestore.dev/docs/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](/docs/reference/devtools)
- Materializer
- Event handler function that maps an event to a state change
- Live queries
- Db queries `queryDb()`
- Computed queries `computed()`
- Events
- Event definition
- Eventlog
- Synced vs client-only events
- Schema
- LiveStore uses schema definitions for the following cases:
- [Event schema](/docs/reference/events/events-schema)
- [SQLite state schema](/docs/reference/state/sqlite-schema)
- [Query result schemas](/docs/reference/state/sql-queries)
- LiveStore uses the [Effect Schema module](/docs/patterns/effect) to define fine-granular schemas
- State
- Derived from the eventlog via materializers
- 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
- Sync backend
- A central server that is responsible for syncing the eventlog between clients
- Framework integration
- A framework integration is a package that provides a way to integrate LiveStore with a framework (e.g. React, Solid, Svelte, etc.)
### Implementation details
- Leader thread
- Responsible for syncing and persisting of data
- Sync processor
- LeaderSyncProcessor
- ClientSessionSyncProcessor
## Architecture diagram
Assuming the web adapter in a multi-client, multi-tab browser application, a diagram looks like this:

The architecture is similar for other adapters (e.g. Expo) but often only involves a single client session per client.
## Pluggable architecture
LiveStore is pluggable in 3 ways:
- Platform adapters
- Sync backends
- Framework integrations
# [Data Modeling](https://livestore.dev/docs/reference/data-modeling/)
## Overview
## Core idea
- Data modeling is probably the most important part of any app and needs to be done carefully.
- The core idea is to model the read and write model separately.
- Depending on the use case, you might also want to split up the read/write model into separate "containers" (e.g. for data-sharing/scalability/access control reasons).
- There is no transactional consistency between containers.
- Caveat: Event sourcing is not ideal for all use cases - some apps might be better off with another approach (e.g. use CRDTs for rich text editing).
## Considerations for data modeling
- How much data do you expect to have and what is the shape of the data?
- Some kind of data needs special handling (e.g. blobs or rich text)
- Access patterns (performance, ...)
- Access control
- Data integrity / consistency
- Sharing / collaboration
- Regulatory requirements (e.g. GDPR, audit logs, ...)
## TODO
- TODO: actually write this section
- questions to answer
- When to split things into separate containers?
- How do migrations work?
- Read model migrations
- Write model migrations
- How to create new write models based on existing ones
- Example: An app has multiple workspaces and you now want to introduce the concept of "projects" inside a workspace. You might want to pre-populate a "default workspace project" for each workspace.
# [AI](https://livestore.dev/docs/patterns/ai/)
## Overview
- LiveStore is a great fit for building AI applications.
- Scenarios:
- Local RAG (via sqlite-vec (see [feature request](https://github.com/livestorejs/livestore/issues/127)) + local LLM e.g. Gemini Nano embedded in Chrome)
- Agentic applications
## Example
```ts
// TODO (contribution welcome)
```
# [File Management](https://livestore.dev/docs/patterns/file-management/)
## Overview
LiveStore doesn't have built-in support for file management but it's easy to use LiveStore alongside existing file storage solutions (e.g. S3).
The basic idea is to store the file metadata (e.g. url, name, size, type) in LiveStore and the file content separately.
## Example
```ts
// TODO (contribution welcome)
```
# [Troubleshooting](https://livestore.dev/docs/misc/troubleshooting/)
## Overview
## 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.12.1" }` 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.
# [Effect](https://livestore.dev/docs/patterns/effect/)
## Overview
LiveStore itself is built on top of [Effect](https://effect.website) which is a powerful library to write production-grade TypeScript code. It's also possible (and recommended) to use Effect directly in your application code.
## Schema
LiveStore uses the [Effect Schema](https://effect.website/docs/schema/introduction/) library to define schemas for the following:
- Read model table column definitions
- Event event payloads definitions
- Query response types
For convenience, LiveStore re-exports the `Schema` module from the `effect` package, which is the same as if you'd import it via `import { Schema } from 'effect'` directly.
### Example
```ts
import { Schema } from '@livestore/livestore'
// which is equivalent to (if you have `effect` as a dependency)
import { Schema } from 'effect'
```
# [ORM](https://livestore.dev/docs/patterns/orm/)
## Overview
- LiveStore has a built-in query builder which should be sufficient for most simple use cases.
- You can always fall back to using raw SQL queries if you need more complex queries.
- As long as the ORM allows supports synchronously generating SQL statements (and binding parameters), you should be able to use it with LiveStore.
- Supported ORMs:
- [Knex](https://knexjs.org/)
- [Kysely](https://kysely.dev/)
- [Drizzle](https://orm.drizzle.team/)
- [Objection.js](https://vincit.github.io/objection.js/)
- Unsupported ORMs:
- [Prisma](https://www.prisma.io/) (because it's async)
## Example
```ts
// TODO (contribution welcome)
```
# [State Machines](https://livestore.dev/docs/patterns/state-machines/)
## Overview
LiveStore can be used to implement state machines or together with existing state machine libraries (e.g. [XState](https://stately.ai/docs/xstate)).
The basic idea is to listen query results and emit events when the query results change. The state machine side effects can then further commit new mutations to LiveStore.
## Example
```ts
// TODO (contribution welcome)
```
# [Undo/Redo](https://livestore.dev/docs/patterns/undo-redo/)
## Overview
Undo/redo functionality should be generally modeled through explicit events instead of "removing" events from the event history.
## Example
```ts
// TODO (contribution welcome)
```
# [File Structure](https://livestore.dev/docs/patterns/file-structure/)
## Overview
While there are no strict requirements/conventions for how to structure your project (files, folders, etc), a common pattern is to have a `src/livestore` folder which contains all the LiveStore related code.
```
src/
livestore/
index.ts # re-exports everything
schema.ts # schema definitions
queries.ts # query definitions
events.ts # event definitions
...
...
```
# [Devtools](https://livestore.dev/docs/reference/devtools/)
## Overview
NOTE: Once LiveStore is open source, the devtools will be a [sponsor-only benefit](/docs/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.
# [Event Sourcing](https://livestore.dev/docs/reference/event-sourcing/)
## Overview
- Similar to Redux but persisted and synced across devices
- Core idea: Separate read vs write model
- Read model: App database (i.e. SQLite)
- Write model: Ordered log of all mutation events
- Related topics
- Domain driven design
- Benefits
- Simple mental model
- Scalable
- Flexible
- You can easily evolve the read model based on your query patterns as your app requirements change over time
- Automatic migrations of the read model (i.e. app database)
- Write model can also be evolved (e.g. via versioned mutations and optionally mapping old mutations to new ones)
- History of all state changes is captured (e.g. for auditing and debugging)
- Foundation for syncing
- Downsides
- Slightly more boilerplate to manually define mutations
- Need to be careful so eventlog doesn't grow too much
## Further reading
- [The Log: What every software engineer should know about real-time data's unifying abstraction](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying)
# [Custom Elements](https://livestore.dev/docs/reference/Framework%20Integrations/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.
# [OpenTelemetry](https://livestore.dev/docs/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 } })
```
# [React integration for LiveStore](https://livestore.dev/docs/reference/Framework%20Integrations/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.
# [Electron Adapter](https://livestore.dev/docs/reference/Platform%20Adapters/electron-adapter/)
## Overview
LiveStore doesn't yet support Electron (see [this issue](https://github.com/livestorejs/livestore/issues/296) for more details).
# [Solid integration](https://livestore.dev/docs/reference/Framework%20Integrations/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.
# [Tauri Adapter](https://livestore.dev/docs/reference/Platform%20Adapters/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://livestore.dev/docs/reference/State/sqlite-schema/)
## Overview
import { Code, Tabs, TabItem } from '@astrojs/starlight/components';
import eventsCode from '../../../../../../../examples/standalone/web-todomvc/src/livestore/events.ts?raw'
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:
- `from-eventlog`: Automatically migrate the database to the newest schema and rehydrates the data from the eventlog.
- `hard-reset`: Automatically migrate the database to the newest schema but ignores 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://livestore.dev/docs/reference/State/sqlite/)
## Overview
LiveStore heavily relies on SQLite as its default read model.
- 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.
## Implementation notes
- 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).
# [Node Adapter](https://livestore.dev/docs/reference/Platform%20Adapters/node-adapter/)
## Overview
Works with Node.js, Bun and Deno.
## Example
```ts
import { makePersistedAdapter } from '@livestore/adapter-node'
const adapter = makePersistedAdapter({
schemaPath: new URL('./schema.ts', import.meta.url),
workerUrl: new URL('./livestore.worker.js', import.meta.url),
})
```
# [Expo Adapter](https://livestore.dev/docs/reference/Platform%20Adapters/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": {}
}
]
}
}
```
# [Reactivity system](https://livestore.dev/docs/reference/State/reactivity-system/)
## Overview
LiveStore provides a Signals-like reactivity system which supports:
- Reactive SQL queries on top of SQLite state (`queryDb()`)
- Reactive computed values (`computed()`)
- Reactive state values (`makeRef()`)
Live query variables end on a `$` by convention (e.g. `todos$`).
### Reactive SQL queries
```ts
import { queryDb } from '@livestore/livestore'
const todos$ = queryDb(tables.todos.orderBy('createdAt', 'desc'))
// Or using callback syntax to depend on other queries
const todos$ = queryDb((get) => {
const { showCompleted } = get(uiState$)
return tables.todos.where(showCompleted ? { completed: true } : {})
})
```
### Computed values
```ts
import { computed } from '@livestore/livestore'
const showAllLabel$ = computed((get) => get(uiState$).showCompleted ? 'show completed' : 'show all')
```
## Further reading
- [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.
# [SQL Queries](https://livestore.dev/docs/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.
# [Web Adapter](https://livestore.dev/docs/reference/Platform%20Adapters/web-adapter/)
## Overview
## Example
```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
import { makeWorker } from '@livestore/adapter-web/worker'
import { schema } from './schema/index.js'
makeWorker({ schema })
```
## 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.
## 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)
# [Limitations](https://livestore.dev/docs/reference/Syncing/limitations/)
## Overview
undefined
# [Server-side clients](https://livestore.dev/docs/reference/Syncing/server-side-clients/)
## Overview
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 data changes on the server side
- run mutations on the server side
## 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.
# [Syncing](https://livestore.dev/docs/reference/Syncing/syncing/)
## Overview
## How it works
LiveStore is based on [the idea of event-sourcing](/docs/reference/event-sourcing) which means it syncs mutation events across clients (via a central sync backend) and then applies the events to the local SQLite database. 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 mutation 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 mutation event consists of the following data:
- `id`: event id
- `parentId`: parent event id
- `mutation`: mutation name (refers to a mutation definition in the schema)
- `args`: mutation arguments (encoded using the mutation's schema definition, usually JSON)
### Event ids
- Event ids: sequence integers
- local event id to sync across client sessions
### Sync heads
- The latest event in a mutation 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 mutation 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 mutation events given a starting event ID (often referred to as cursor).
- Ideally provides a "reactivity" mechanism to notify clients when new mutation 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
### Example sequence diagram (TODO)
```mermaid
```
### 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 a mutation event log 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)
# [Events Schema](https://livestore.dev/docs/reference/Events/events-schema/)
## Overview
```ts
import { Events, Schema, sql } from '@livestore/livestore'
export const todoCreated = Events.synced({
name: 'todoCreated',
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
})
export const todoCompleted = Events.synced({
name: 'todoCompleted',
schema: Schema.Struct({ id: Schema.String }),
})
```
# [Events](https://livestore.dev/docs/reference/Events/events/)
## Overview
import { Card } from '@astrojs/starlight/components'
This page is needs to be rewritten. Please reach out in Discord if you have any related questions.
## Commiting events
```ts
// events.ts
import { Events } from '@livestore/livestore'
export const todoCreated = Events.synced({
name: 'todoCreated',
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
})
// somewhere in your app
store.commit(
todoCreated({ id: '1', text: 'Buy milk' })
)
```
## 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 mutations adhere to them + get suggestions if not
- It's recommended to avoid `DELETE` mutations 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.
## Rules of Mutations
### Schema evolution
- Mutation definitions can't be removed after they were added to your app.
- Mutation 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
### Derived mutations
- When using `enabledCud` on a table definition, LiveStore will automatically generate some mutations for you. This includes a insert, update, and delete event.
### Other Notes
- Mutations need to be deterministic. This means that the same input will always produce the same output.
- When re-using existing schema definitions for event schemas, be careful to also apply the rules of mutations to the schema definitions you are re-using. A more conservative approach is to copy the schema definitions and evolve them separately.
### Syncing related
- SQL writes need to be deterministic e.g. have no branching/reading.
### Non-persisted mutations
Sometimes it can be useful to not persist mutations but still sync them across clients (e.g. for frequent in-progress updates like position updates during a drag & drop interaction). Calling `store.commit({ persisted: false }, myMutation)` will still apply and sync the event but won't persist it in the eventlog and in the sync server.
It's important that the "final event" sets explicit values (and e.g. doesn't increment an existing value) i.e. shouldn't care about what the current value is.
# [ElectricSQL](https://livestore.dev/docs/reference/Syncing/Sync%20Backends/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 backend works
The initial version of the ElectricSQL sync backend 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 backend?
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.
# [Cloudflare Workers](https://livestore.dev/docs/reference/Syncing/Sync%20Backends/cloudflare/)
## Overview
## Example
### 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')
}
},
})
```
## 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`.