The big picture
When a user performs an action in your app, LiveStore:- Commits an immutable event describing what happened
- Persists the event to the local eventlog
- Runs materializers to update the local SQLite database
- Notifies the reactivity system, which re-renders subscribed UI components
- Pushes the event to the sync backend so other clients receive it
Platform adapters
LiveStore is designed to run on multiple platforms. A platform adapter abstracts over the runtime environment and is responsible for:- Spawning and managing the leader thread (Web Worker, background thread, or in-process)
- Providing access to persistent storage (SQLite file, IndexedDB-backed storage)
- Wiring up the communication channel between the client session thread and the leader thread
Web adapter
@livestore/adapter-web — runs in browsers. The leader thread runs in a Web Worker. Supports both in-memory and persisted SQLite.Expo adapter
@livestore/adapter-expo — runs on iOS and Android via Expo. Uses the device’s native SQLite for persistence.Node adapter
@livestore/adapter-node — runs in Node.js for server-side apps, CLI tools, and AI agents.Client-side event flow
On the client, LiveStore maintains two SQLite databases and a reactivity graph:| Database | Location | Purpose |
|---|---|---|
| In-memory SQLite | Client session thread (main thread) | Powers the reactivity graph — synchronous, instant reads |
| Persisted SQLite | Leader thread (Web Worker) | Durable storage across sessions, source for rematerialization |
Step-by-step: from user action to UI update
Commit an event
Your application calls
store.commit(events.todoCreated({ id, text })). This records an immutable event — a named, typed fact about something that happened.Persist to the eventlog
The event is appended to the local eventlog on the leader thread. The eventlog is an ordered, append-only sequence of all synced events. It is the canonical source of truth.
Materialize state
The leader thread runs the matching materializer — a pure function that translates the event into a SQLite write (insert, update, or delete). The persisted SQLite database is updated atomically with the eventlog append.
Propagate to the in-memory replica
The state change is forwarded to the in-memory SQLite database on the client session thread (main thread). This replica is kept in sync with the persisted database.
Leader thread vs. client session thread
The separation between the leader thread and the client session thread is central to LiveStore’s performance and correctness.Leader thread
The leader thread (a Web Worker in browsers) is responsible for:- Persisting events to the local eventlog
- Running materializers to update the persisted SQLite database
- Communicating with the sync backend — pushing local events and pulling remote events
- Conflict resolution — rebasing local pending events on top of incoming remote events
Client session thread
The client session thread (usually the main thread) is responsible for:- Holding the in-memory SQLite replica used for all reads
- Running the reactivity graph — tracking subscriptions and notifying UI components
- Accepting
store.commit()calls from application code and forwarding events to the leader
The platform adapter manages the leader election and thread communication automatically. You don’t need to think about it for typical application development.
Sync architecture
LiveStore extends the local event-sourcing model globally by synchronizing the eventlog across all clients through a central sync backend.Push/pull model
Inspired by Git, LiveStore uses a push/pull model for event synchronization:Local commit (optimistic)
When you commit an event, it is applied to the local SQLite database immediately. The UI updates before any network round-trip — this is the optimistic update.
Pull before push
Before pushing a local event to the sync backend, the leader pulls the latest events from the backend. This ensures the local eventlog is up to date and prevents ordering conflicts.
Rebase if needed
If new remote events arrived while you were working, your local pending events are rebased on top of them. Materializers re-run in the correct global order.
Push to backend
Once the local eventlog is consistent with the backend, the pending local events are pushed. The backend enforces a global total order.
Sync data flow diagram
Offline behavior
When there is no network connectivity,store.commit() still works — events are committed to the local eventlog and materialized immediately. Pending events accumulate in the local eventlog. When connectivity returns, the leader thread syncs all pending events to the backend automatically.
Your app requires no special offline handling code.
Conflict resolution
Concurrent operations from different clients can produce conflicting events. LiveStore resolves these via rebase:- When client A’s push is rejected because client B pushed first, client A pulls client B’s events.
- Client A’s local pending events are removed from the tail of the eventlog.
- Client B’s events are appended in their globally-ordered position.
- Client A’s pending events are re-appended after client B’s events and materialized again.
SQLite: in-memory vs. persisted
LiveStore uses two SQLite databases per client, each serving a different purpose:In-memory SQLite (client session thread)
- Lives on the main thread — no async I/O
- Populated from the persisted database on startup, then kept in sync via incremental updates
- Powers all reactive queries (
queryDb,computed) - Discarded when the session ends
Persisted SQLite (leader thread)
- Lives on the leader thread (Web Worker or native thread)
- Written atomically with each eventlog append
- Survives page refreshes and app restarts
- Can be fully reconstructed by replaying the eventlog from scratch