# Introduction

import CapabilitiesDiagram from '../../_assets/diagrams/capabilities.tldr?tldraw';




## What is LiveStore?

LiveStore is an event-driven data layer with a built-in sync engine. Its prime use case is building complex, client-side apps like [Linear](http://linear.app/), [Figma](https://www.figma.com/) or [Notion](https://notion.so/) that also work offline.

Think of LiveStore as a next-generation state management library (like Zustand or Redux) that also persists and distributes state:

<CapabilitiesDiagram class="my-8" />



The combination of persisted and distributed state is a giant leap for creating an **amazing user experience** (UX), while also providing a **best-in-class developer experience** (DX). The **immutable eventlog** enables robust testing and tight feedback loops, making it perfect for agentic coding. 



<div class="not-content grid grid-cols-1 md:grid-cols-3 gap-4 my-8">
  <div class="rounded-xl border border-gray-700 p-5">
    <h3 class="text-base font-medium !mt-0 !mb-2">UX</h3>
    <ul class="!space-y-2 text-sm list-none p-0 m-0">
      <li><strong>Synced</strong>: Real-time updates across devices</li>
      <li><strong>Fast</strong>: No async loading over the network</li>
      <li><strong>Persistent</strong>: Works offline, survives page refresh</li>
    </ul>
  </div>

  <div class="rounded-xl border border-gray-700 p-5">
    <h3 class="text-base font-medium !mt-0 !mb-2">DX</h3>
    <ul class="!space-y-2 text-sm list-none p-0 m-0">
      <li><strong>Principled</strong>: Event-driven instead of mutable state</li>
      <li><strong>Composable & Type-safe</strong>: Fully typed events & queries</li>
      <li><strong>Reactive</strong>: Automatic UI updates when data changes</li>
    </ul>
  </div>

  <div class="rounded-xl border border-gray-700 p-5">
    <h3 class="text-base font-medium !mt-0 !mb-2">AX</h3>
    <ul class="!space-y-2 text-sm list-none p-0 m-0">
      <li><strong>Testable</strong>: Immutable eventlog for feedback loop</li>
      <li><strong>Debuggable</strong>: Same events always produce same state</li>
      <li><strong>Evolvable</strong>: Reset & fork state for experiments</li>
    </ul>
  </div>
</div>

LiveStore works cross-platform and can be used for building UI apps (web, mobile, desktop, ...), agents, and any other software like CLIs, scripts or server-to-server applications.

See this talk for more info: [Sync different: Event sourcing in local-first apps](https://www.youtube.com/watch?v=nyPl84BopKc&list=PL4isNRKAwz2MabH6AMhUz1yS3j1DqGdtT).

<details>
<summary>Are you an expert? See here for advanced topics.</summary>
- [How LiveStore deals with merge conflicts?](/overview/how-livestore-works#conflict-resolution)
- Why event-sourcing instead of CRDTs or query-driven sync?
- How do schema migrations work?
- What are limitations of LiveStore?
- How LiveStore is perfect for coding agents.
</details>

## The core idea: Synced events -> State -> UI

Unlike other sync solutions, LiveStore syncs events—not state!

Events are immutable facts that describe what happened ("TodoCreated", "TodoCompleted"), while state is derived by replaying them. This means every client reconstructs the same state from the same event history, making sync predictable and debuggable.

The state then changes trigger reaactive UI updates.

### Traditional state management uses ephemeral, in-memory state

Traditional state management works like this: you dispatch actions that update an in-memory store, and your UI reacts to changes. But that state vanishes when the user refreshes or closes the browser. Add persistence and you need to manage local storage which essentially serves as a secondary database to the data you've stored in the cloud. Add sync and you're dealing with conflict resolution, offline queues, and backend integration. LiveStore handles all this for you with one simple, event-driven API!

### LiveStore persists events which materialize into state

LiveStore handles all of this through one unified pattern: **event sourcing**.

<div class="d2-full-width">

```d2
...@../../../../src/content/base.d2

direction: right

"User action": {
  label: "User action"
  shape: rectangle
}

Event: {
  label: "Event"
  shape: rectangle
}

Eventlog: {
  label: "Eventlog"
  shape: rectangle
}

"SQLite state": {
  label: "SQLite state"
  shape: rectangle
}

UI: {
  label: "UI"
  shape: rectangle
}

"Sync to other clients": {
  label: "Sync to other clients"
  shape: rectangle
}

"User action" -> Event -> Eventlog -> "SQLite state" -> UI
Eventlog -> "Sync to other clients"
```

</div>

Instead of mutating state directly, you commit **events** that _describe_ what happened. These events are persisted to an **eventlog** (like a git history) and automatically materialized into a local **SQLite database** that your UI queries reactively.

:::note
While the majority of apps will probably use SQLite as the data store for persistence, LiveStore is flexible enough to materialize state into other targets as well (e.g. file systems).
:::

If you want to learn more, you can dive deeper into [how LiveStore works](/overview/how-livestore-works).

### Comparison with traditional state management like Redux

If you've used Redux, this pattern of "comitting events" will feel familiar: **Events are like actions, materializers are like reducers, and the SQLite state is like your store.**

But there are key differences:

| Redux                                              | LiveStore                                   |
| -------------------------------------------------- | ------------------------------------------- |
| Actions dispatch → reducers update in-memory state | Events commit → materializers update SQLite |
| State lost on refresh                              | Events persisted locally                    |
| Sync requires external setup                       | Sync built-in via eventlog                  |
| Fixed state shape                                  | Query any shape with SQL                    |

## A practical example

Let's walk through a simple example of a todo list with LiveStore and React.

### Define your schema

At the core of every app built with LiveStore, you have a _schema_ which consists of three parts:

- **Events**: describe what can happen in your app
- **State**: defines how data is stored in your app
- **Materializers**: determines how events are mapped to state in your app

Here's an example:


## `reference/overview/introduction/schema.ts`

```ts filename="reference/overview/introduction/schema.ts"
// schema.ts
import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// 1. Define events (the things that can happen in your app)
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 }),
  }),
}

// 2. Define SQLite tables (how to query your state)
export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text({ default: '' }),
      completed: State.SQLite.boolean({ default: false }),
    },
  }),
}

// 3. Define materializers (how to turn events into state)
const materializers = State.SQLite.materializers(events, {
  'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text }),
  'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
})

const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })
```

### Usage on the frontend

LiveStore comes with integrations for all major frontend frameworks, e.g. for [React](/framework-integrations/react-integration) or [Vue](/framework-integrations/vue-integration).

The [`queryDb`](/building-with-livestore/reactivity-system#reactive-sql-queries) function creates a [reactive query](/building-with-livestore/reactivity-system) which updates automatically when its data in the database changes. Here's how to use it in React:


## `reference/overview/introduction/todo-app.tsx`

```tsx filename="reference/overview/introduction/todo-app.tsx"
import { useCallback } from 'react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'

// TodoApp.tsx
import { makeInMemoryAdapter } from '@livestore/adapter-web'
import { queryDb } from '@livestore/livestore'
import { useStore } from '@livestore/react'

import { events, schema, tables } from './schema.ts'

const adapter = makeInMemoryAdapter()

const useAppStore = () =>
  useStore({
    storeId: 'my-app',
    schema,
    adapter,
    batchUpdates,
  })

// Define a reactive query
const visibleTodos$ = queryDb(() => tables.todos, {
  label: 'visibleTodos',
})

type Todo = {
  id: string
  text: string
  completed: boolean
}

export const TodoApp = () => {
  const store = useAppStore()

  // Reactively updates when todos change in the DB
  const todos = store.useQuery(visibleTodos$)

  const addTodo = useCallback(
    (text: string) => {
      // Commit an event to the store
      store.commit(
        events.todoCreated({
          id: crypto.randomUUID(),
          text,
        }),
      )
    },
    [store],
  )

  const completeTodo = useCallback(
    (id: string) => {
      // Commit an event to the store
      store.commit(events.todoCompleted({ id }))
    },
    [store],
  )

  const handleAddTodo = useCallback(() => {
    addTodo('New todo')
  }, [addTodo])

  return (
    <div>
      <button type="button" onClick={handleAddTodo}>
        Add
      </button>
      {todos.map((todo) => (
        <TodoListItem key={todo.id} todo={todo} onComplete={completeTodo} />
      ))}
    </div>
  )
}

const TodoListItem = ({ todo, onComplete }: { todo: Todo; onComplete: (id: string) => void }) => {
  const handleComplete = useCallback(() => {
    onComplete(todo.id)
  }, [onComplete, todo.id])

  return (
    <button type="button" onClick={handleComplete}>
      {todo.completed === true ? '✓' : '○'} {todo.text}
    </button>
  )
}
```

### `reference/overview/introduction/schema.ts`

```ts filename="reference/overview/introduction/schema.ts"
// schema.ts
import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// 1. Define events (the things that can happen in your app)
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 }),
  }),
}

// 2. Define SQLite tables (how to query your state)
export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text({ default: '' }),
      completed: State.SQLite.boolean({ default: false }),
    },
  }),
}

// 3. Define materializers (how to turn events into state)
const materializers = State.SQLite.materializers(events, {
  'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text }),
  'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
})

const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })
```

This code replaces all the API calls, state management, UI update, caching, and synchronization logic you may be used to from writing apps in a more traditional way. If configured, it also automatically takes care of syncing data to other clients.

Also notice how there's no loading state for queries—SQLite runs in-memory on the main thread, so reads are synchronous and instant, making your app super snappy and reactive to user input.

## Why events?

But why use events at all, rather than directly mutating state?

### Benefits of using events for state management

- **Events capture intent, not just outcome.** When you mutate state directly (`todo.completed = true`), you lose the _why_. Events like `TodoCompleted` preserve the user's intent, which matters for debugging, analytics, undo/redo, and features like activity feeds.
- **Events decouple what happened from how it's stored.** Your state shape can evolve independently of your event history. Need a new denormalized table for performance? Just add a materializer—no data migration required.
- **Events make sync tractable.** Syncing mutable state across devices is hard (which field wins?). Syncing an append-only event log is simpler: you're merging histories, not reconciling conflicting states.

### Benefits of LiveStore's eventlog 

The [eventlog](/building-with-livestore/events/#eventlog) sits the core of LiveStore and gives you several benefits:

- **Persistence**: Events survive page refreshes and app restarts
- **Sync**: Events replay identically across devices
- **History**: Full audit trail of every change (enables time travel and helps with debugging)
- **Flexibility**: Change your queries without migrations—just materialize differently

## How syncing works

LiveStore includes a **sync engine** which handles [syncing](/building-with-livestore/syncing) for you under the hood. 

When you can enable syncing, the sync engine distributes your state across various other clients (these can be other browsers, apps, devices, servers—anything that can connect to your store via an [adapter](/overview/how-livestore-works#platform-adapters)).

LiveStore uses a push/pull model inspired by git:

1. Local events are committed immediately (optimistic updates by default)
1. Events sync to a central backend when online
1. Other clients pull new events and materialize them locally
1. Conflicts resolve deterministically (last-write-wins by default, or custom logic)

Here's an example that syncs your state via Cloudflare (using the [`@livestore/sync-cf`](/sync-providers/cloudflare/) package):


## `reference/overview/introduction/worker.ts`

```ts filename="reference/overview/introduction/worker.ts"
import { makeWorker } from '@livestore/adapter-web/worker'
import { makeWsSync } from '@livestore/sync-cf/client'

import { schema } from './schema.ts'

makeWorker({
  schema,
  sync: {
    backend: makeWsSync({ url: `${location.origin}/sync` }),
  },
})
```

### `reference/overview/introduction/schema.ts`

```ts filename="reference/overview/introduction/schema.ts"
// schema.ts
import { Events, makeSchema, Schema, State } from '@livestore/livestore'

// 1. Define events (the things that can happen in your app)
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 }),
  }),
}

// 2. Define SQLite tables (how to query your state)
export const tables = {
  todos: State.SQLite.table({
    name: 'todos',
    columns: {
      id: State.SQLite.text({ primaryKey: true }),
      text: State.SQLite.text({ default: '' }),
      completed: State.SQLite.boolean({ default: false }),
    },
  }),
}

// 3. Define materializers (how to turn events into state)
const materializers = State.SQLite.materializers(events, {
  'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text }),
  'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
})

const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })
```

That's all the configuration needed—your app works offline automatically. When connectivity returns, LiveStore syncs pending events and reconciles state.

## When LiveStore fits

LiveStore works well for:

- **Productivity apps** (todo lists, note-taking, project management, ...)
- **Collaborative tools** with multi-player support
- **Apps with complex local state** that need SQL-level queries
- **Local-first** apps with offline support
- **Cross-platform apps** (web, mobile, desktop, server)
- ... or any combination of the above

LiveStore may not fit if:

- Your data must live on an existing server database (consider [ElectricSQL](https://electric-sql.com) or [Zero](https://zero.rocicorp.dev))
- You're building a traditional client-server app without offline needs
- You need unbounded data that won't fit in client memory

If you're unsure, go through our [evaluation exercise](/overview/when-livestore) to find out whether LiveStore is a good fit for your project or [compare it with similar tools](/overview/technology-comparison).

## Next steps

- [Tutorial](/tutorial/0-welcome) — Step-by-step tutorial introducing the main concepts and workflows of LiveStore
- [Getting started with React](/getting-started/react-web) — Quickstart to set up a React app
- [How LiveStore works](/overview/how-livestore-works) — Deeper dive into the LiveStore architecture
- [Examples](/examples) — See LiveStore in action with TodoMVC, Linearlite, and more