@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/effect/LiveStore.d.ts +123 -2
- package/dist/effect/LiveStore.d.ts.map +1 -1
- package/dist/effect/LiveStore.js +195 -1
- package/dist/effect/LiveStore.js.map +1 -1
- package/dist/effect/mod.d.ts +1 -1
- package/dist/effect/mod.d.ts.map +1 -1
- package/dist/effect/mod.js +3 -1
- package/dist/effect/mod.js.map +1 -1
- package/dist/mod.d.ts +1 -0
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +1 -0
- package/dist/mod.js.map +1 -1
- package/dist/store/StoreRegistry.d.ts +190 -0
- package/dist/store/StoreRegistry.d.ts.map +1 -0
- package/dist/store/StoreRegistry.js +244 -0
- package/dist/store/StoreRegistry.js.map +1 -0
- package/dist/store/StoreRegistry.test.d.ts +2 -0
- package/dist/store/StoreRegistry.test.d.ts.map +1 -0
- package/dist/store/StoreRegistry.test.js +380 -0
- package/dist/store/StoreRegistry.test.js.map +1 -0
- package/dist/store/create-store.d.ts +50 -4
- package/dist/store/create-store.d.ts.map +1 -1
- package/dist/store/create-store.js +19 -0
- package/dist/store/create-store.js.map +1 -1
- package/dist/store/devtools.d.ts.map +1 -1
- package/dist/store/devtools.js +13 -0
- package/dist/store/devtools.js.map +1 -1
- package/dist/store/store-types.d.ts +10 -25
- package/dist/store/store-types.d.ts.map +1 -1
- package/dist/store/store-types.js.map +1 -1
- package/dist/store/store.d.ts +23 -6
- package/dist/store/store.d.ts.map +1 -1
- package/dist/store/store.js +20 -2
- package/dist/store/store.js.map +1 -1
- package/docs/building-with-livestore/complex-ui-state/index.md +0 -2
- package/docs/building-with-livestore/crud/index.md +0 -2
- package/docs/building-with-livestore/data-modeling/index.md +29 -0
- package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -6
- package/docs/building-with-livestore/opentelemetry/index.md +25 -6
- package/docs/building-with-livestore/rules-for-ai-agents/index.md +2 -2
- package/docs/building-with-livestore/state/sql-queries/index.md +22 -0
- package/docs/building-with-livestore/state/sqlite-schema/index.md +2 -2
- package/docs/building-with-livestore/store/index.md +344 -0
- package/docs/framework-integrations/react-integration/index.md +380 -361
- package/docs/framework-integrations/vue-integration/index.md +2 -2
- package/docs/getting-started/expo/index.md +189 -43
- package/docs/getting-started/react-web/index.md +77 -24
- package/docs/getting-started/vue/index.md +3 -3
- package/docs/index.md +1 -2
- package/docs/llms.txt +0 -1
- package/docs/misc/troubleshooting/index.md +3 -3
- package/docs/overview/how-livestore-works/index.md +1 -1
- package/docs/overview/introduction/index.md +409 -1
- package/docs/overview/why-livestore/index.md +108 -2
- package/docs/patterns/auth/index.md +185 -34
- package/docs/patterns/effect/index.md +11 -1
- package/docs/patterns/storybook/index.md +43 -26
- package/docs/platform-adapters/expo-adapter/index.md +36 -19
- package/docs/platform-adapters/web-adapter/index.md +71 -2
- package/docs/tutorial/1-setup-starter-project/index.md +5 -5
- package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +54 -35
- package/docs/tutorial/5-expand-business-logic/index.md +1 -1
- package/docs/tutorial/6-persist-ui-state/index.md +12 -12
- package/package.json +6 -6
- package/src/effect/LiveStore.ts +385 -3
- package/src/effect/mod.ts +13 -1
- package/src/mod.ts +1 -0
- package/src/store/StoreRegistry.test.ts +516 -0
- package/src/store/StoreRegistry.ts +393 -0
- package/src/store/create-store.ts +50 -4
- package/src/store/devtools.ts +15 -0
- package/src/store/store-types.ts +17 -5
- package/src/store/store.ts +25 -5
- package/docs/building-with-livestore/examples/index.md +0 -30
|
@@ -2,4 +2,412 @@
|
|
|
2
2
|
|
|
3
3
|
## What is LiveStore?
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
Think of LiveStore as a next-generation state management library (like Zustand or Redux) that also persists and distributes state:
|
|
8
|
+
|
|
9
|
+
<CapabilitiesDiagram class="my-8" />
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
<div class="not-content grid grid-cols-1 md:grid-cols-3 gap-4 my-8">
|
|
14
|
+
<div class="rounded-xl border border-gray-700 p-5">
|
|
15
|
+
<h3 class="text-base font-medium !mt-0 !mb-2">UX</h3>
|
|
16
|
+
<ul class="!space-y-2 text-sm list-none p-0 m-0">
|
|
17
|
+
<li><strong>Synced</strong>: Real-time updates across devices</li>
|
|
18
|
+
<li><strong>Fast</strong>: No async loading over the network</li>
|
|
19
|
+
<li><strong>Persistent</strong>: Works offline, survives page refresh</li>
|
|
20
|
+
</ul>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="rounded-xl border border-gray-700 p-5">
|
|
24
|
+
<h3 class="text-base font-medium !mt-0 !mb-2">DX</h3>
|
|
25
|
+
<ul class="!space-y-2 text-sm list-none p-0 m-0">
|
|
26
|
+
<li><strong>Principled</strong>: Event-driven instead of mutable state</li>
|
|
27
|
+
<li><strong>Composable & Type-safe</strong>: Fully typed events & queries</li>
|
|
28
|
+
<li><strong>Reactive</strong>: Automatic UI updates when data changes</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="rounded-xl border border-gray-700 p-5">
|
|
33
|
+
<h3 class="text-base font-medium !mt-0 !mb-2">AX</h3>
|
|
34
|
+
<ul class="!space-y-2 text-sm list-none p-0 m-0">
|
|
35
|
+
<li><strong>Testable</strong>: Immutable eventlog for feedback loop</li>
|
|
36
|
+
<li><strong>Debuggable</strong>: Same events always produce same state</li>
|
|
37
|
+
<li><strong>Evolvable</strong>: Reset & fork state for experiments</li>
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
See this talk for more info: [Sync different: Event sourcing in local-first apps](https://www.youtube.com/watch?v=nyPl84BopKc&list=PL4isNRKAwz2MabH6AMhUz1yS3j1DqGdtT).
|
|
45
|
+
|
|
46
|
+
<details>
|
|
47
|
+
<summary>Are you an expert? See here for advanced topics.</summary>
|
|
48
|
+
- [How LiveStore deals with merge conflicts?](/overview/how-livestore-works#conflict-resolution)
|
|
49
|
+
- Why event-sourcing instead of CRDTs or query-driven sync?
|
|
50
|
+
- How do schema migrations work?
|
|
51
|
+
- What are limitations of LiveStore?
|
|
52
|
+
- How LiveStore is perfect for coding agents.
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
## The core idea: Synced events -> State -> UI
|
|
56
|
+
|
|
57
|
+
Unlike other sync solutions, LiveStore syncs events—not state!
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
The state then changes trigger reaactive UI updates.
|
|
62
|
+
|
|
63
|
+
### Traditional state management uses ephemeral, in-memory state
|
|
64
|
+
|
|
65
|
+
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!
|
|
66
|
+
|
|
67
|
+
### LiveStore persists events which materialize into state
|
|
68
|
+
|
|
69
|
+
LiveStore handles all of this through one unified pattern: **event sourcing**.
|
|
70
|
+
|
|
71
|
+
<div class="d2-full-width">
|
|
72
|
+
|
|
73
|
+
```d2
|
|
74
|
+
...@../../../../src/content/base.d2
|
|
75
|
+
|
|
76
|
+
direction: right
|
|
77
|
+
|
|
78
|
+
"User action": {
|
|
79
|
+
label: "User action"
|
|
80
|
+
shape: rectangle
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Event: {
|
|
84
|
+
label: "Event"
|
|
85
|
+
shape: rectangle
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Eventlog: {
|
|
89
|
+
label: "Eventlog"
|
|
90
|
+
shape: rectangle
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
"SQLite state": {
|
|
94
|
+
label: "SQLite state"
|
|
95
|
+
shape: rectangle
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
UI: {
|
|
99
|
+
label: "UI"
|
|
100
|
+
shape: rectangle
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
"Sync to other clients": {
|
|
104
|
+
label: "Sync to other clients"
|
|
105
|
+
shape: rectangle
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
"User action" -> Event -> Eventlog -> "SQLite state" -> UI
|
|
109
|
+
Eventlog -> "Sync to other clients"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
:::note
|
|
117
|
+
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).
|
|
118
|
+
:::
|
|
119
|
+
|
|
120
|
+
If you want to learn more, you can dive deeper into [how LiveStore works](/overview/how-livestore-works).
|
|
121
|
+
|
|
122
|
+
### Comparison with traditional state management like Redux
|
|
123
|
+
|
|
124
|
+
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.**
|
|
125
|
+
|
|
126
|
+
But there are key differences:
|
|
127
|
+
|
|
128
|
+
| Redux | LiveStore |
|
|
129
|
+
| -------------------------------------------------- | ------------------------------------------- |
|
|
130
|
+
| Actions dispatch → reducers update in-memory state | Events commit → materializers update SQLite |
|
|
131
|
+
| State lost on refresh | Events persisted locally |
|
|
132
|
+
| Sync requires external setup | Sync built-in via eventlog |
|
|
133
|
+
| Fixed state shape | Query any shape with SQL |
|
|
134
|
+
|
|
135
|
+
## A practical example
|
|
136
|
+
|
|
137
|
+
Let's walk through a simple example of a todo list with LiveStore and React.
|
|
138
|
+
|
|
139
|
+
### Define your schema
|
|
140
|
+
|
|
141
|
+
At the core of every app built with LiveStore, you have a _schema_ which consists of three parts:
|
|
142
|
+
|
|
143
|
+
- **Events**: describe what can happen in your app
|
|
144
|
+
- **State**: defines how data is stored in your app
|
|
145
|
+
- **Materializers**: determines how events are mapped to state in your app
|
|
146
|
+
|
|
147
|
+
Here's an example:
|
|
148
|
+
|
|
149
|
+
## `reference/overview/introduction/schema.ts`
|
|
150
|
+
|
|
151
|
+
```ts filename="reference/overview/introduction/schema.ts"
|
|
152
|
+
// schema.ts
|
|
153
|
+
|
|
154
|
+
// 1. Define events (the things that can happen in your app)
|
|
155
|
+
export const events = {
|
|
156
|
+
todoCreated: Events.synced({
|
|
157
|
+
name: 'v1.TodoCreated',
|
|
158
|
+
schema: Schema.Struct({
|
|
159
|
+
id: Schema.String,
|
|
160
|
+
text: Schema.String,
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
todoCompleted: Events.synced({
|
|
164
|
+
name: 'v1.TodoCompleted',
|
|
165
|
+
schema: Schema.Struct({ id: Schema.String }),
|
|
166
|
+
}),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 2. Define SQLite tables (how to query your state)
|
|
170
|
+
export const tables = {
|
|
171
|
+
todos: State.SQLite.table({
|
|
172
|
+
name: 'todos',
|
|
173
|
+
columns: {
|
|
174
|
+
id: State.SQLite.text({ primaryKey: true }),
|
|
175
|
+
text: State.SQLite.text({ default: '' }),
|
|
176
|
+
completed: State.SQLite.boolean({ default: false }),
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 3. Define materializers (how to turn events into state)
|
|
182
|
+
const materializers = State.SQLite.materializers(events, {
|
|
183
|
+
'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text }),
|
|
184
|
+
'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const state = State.SQLite.makeState({ tables, materializers })
|
|
188
|
+
export const schema = makeSchema({ events, state })
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Usage on the frontend
|
|
192
|
+
|
|
193
|
+
LiveStore comes with integrations for all major frontend frameworks, e.g. for [React](/framework-integrations/react-integration) or [Vue](/framework-integrations/vue-integration).
|
|
194
|
+
|
|
195
|
+
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:
|
|
196
|
+
|
|
197
|
+
## `reference/overview/introduction/todo-app.tsx`
|
|
198
|
+
|
|
199
|
+
```tsx filename="reference/overview/introduction/todo-app.tsx"
|
|
200
|
+
// TodoApp.tsx
|
|
201
|
+
|
|
202
|
+
const adapter = makeInMemoryAdapter()
|
|
203
|
+
|
|
204
|
+
const useAppStore = () =>
|
|
205
|
+
useStore({
|
|
206
|
+
storeId: 'my-app',
|
|
207
|
+
schema,
|
|
208
|
+
adapter,
|
|
209
|
+
batchUpdates,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Define a reactive query
|
|
213
|
+
const visibleTodos$ = queryDb(() => tables.todos, {
|
|
214
|
+
label: 'visibleTodos',
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
export function TodoApp() {
|
|
218
|
+
const store = useAppStore()
|
|
219
|
+
|
|
220
|
+
// Reactively updates when todos change in the DB
|
|
221
|
+
const todos = store.useQuery(visibleTodos$)
|
|
222
|
+
|
|
223
|
+
const addTodo = (text: string) => {
|
|
224
|
+
// Commit an event to the store
|
|
225
|
+
store.commit(
|
|
226
|
+
events.todoCreated({
|
|
227
|
+
id: crypto.randomUUID(),
|
|
228
|
+
text,
|
|
229
|
+
}),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const completeTodo = (id: string) => {
|
|
234
|
+
// Commit an event to the store
|
|
235
|
+
store.commit(events.todoCompleted({ id }))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div>
|
|
240
|
+
<button type="button" onClick={() => addTodo('New todo')}>
|
|
241
|
+
Add
|
|
242
|
+
</button>
|
|
243
|
+
{todos.map((todo) => (
|
|
244
|
+
<button key={todo.id} type="button" onClick={() => completeTodo(todo.id)}>
|
|
245
|
+
{todo.completed ? '✓' : '○'} {todo.text}
|
|
246
|
+
</button>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### `reference/overview/introduction/schema.ts`
|
|
254
|
+
|
|
255
|
+
```ts filename="reference/overview/introduction/schema.ts"
|
|
256
|
+
// schema.ts
|
|
257
|
+
|
|
258
|
+
// 1. Define events (the things that can happen in your app)
|
|
259
|
+
export const events = {
|
|
260
|
+
todoCreated: Events.synced({
|
|
261
|
+
name: 'v1.TodoCreated',
|
|
262
|
+
schema: Schema.Struct({
|
|
263
|
+
id: Schema.String,
|
|
264
|
+
text: Schema.String,
|
|
265
|
+
}),
|
|
266
|
+
}),
|
|
267
|
+
todoCompleted: Events.synced({
|
|
268
|
+
name: 'v1.TodoCompleted',
|
|
269
|
+
schema: Schema.Struct({ id: Schema.String }),
|
|
270
|
+
}),
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 2. Define SQLite tables (how to query your state)
|
|
274
|
+
export const tables = {
|
|
275
|
+
todos: State.SQLite.table({
|
|
276
|
+
name: 'todos',
|
|
277
|
+
columns: {
|
|
278
|
+
id: State.SQLite.text({ primaryKey: true }),
|
|
279
|
+
text: State.SQLite.text({ default: '' }),
|
|
280
|
+
completed: State.SQLite.boolean({ default: false }),
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 3. Define materializers (how to turn events into state)
|
|
286
|
+
const materializers = State.SQLite.materializers(events, {
|
|
287
|
+
'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text }),
|
|
288
|
+
'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const state = State.SQLite.makeState({ tables, materializers })
|
|
292
|
+
export const schema = makeSchema({ events, state })
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
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.
|
|
296
|
+
|
|
297
|
+
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.
|
|
298
|
+
|
|
299
|
+
## Why events?
|
|
300
|
+
|
|
301
|
+
But why use events at all, rather than directly mutating state?
|
|
302
|
+
|
|
303
|
+
### Benefits of using events for state management
|
|
304
|
+
|
|
305
|
+
- **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.
|
|
306
|
+
- **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.
|
|
307
|
+
- **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.
|
|
308
|
+
|
|
309
|
+
### Benefits of LiveStore's eventlog
|
|
310
|
+
|
|
311
|
+
The [eventlog](/building-with-livestore/events/#eventlog) sits the core of LiveStore and gives you several benefits:
|
|
312
|
+
|
|
313
|
+
- **Persistence**: Events survive page refreshes and app restarts
|
|
314
|
+
- **Sync**: Events replay identically across devices
|
|
315
|
+
- **History**: Full audit trail of every change (enables time travel and helps with debugging)
|
|
316
|
+
- **Flexibility**: Change your queries without migrations—just materialize differently
|
|
317
|
+
|
|
318
|
+
## How syncing works
|
|
319
|
+
|
|
320
|
+
LiveStore includes a **sync engine** which handles [syncing](/building-with-livestore/syncing) for you under the hood.
|
|
321
|
+
|
|
322
|
+
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)).
|
|
323
|
+
|
|
324
|
+
LiveStore uses a push/pull model inspired by git:
|
|
325
|
+
|
|
326
|
+
1. Local events are committed immediately (optimistic updates by default)
|
|
327
|
+
1. Events sync to a central backend when online
|
|
328
|
+
1. Other clients pull new events and materialize them locally
|
|
329
|
+
1. Conflicts resolve deterministically (last-write-wins by default, or custom logic)
|
|
330
|
+
|
|
331
|
+
Here's an example that syncs your state via Cloudflare (using the [`@livestore/sync-cf`](/sync-providers/cloudflare/) package):
|
|
332
|
+
|
|
333
|
+
## `reference/overview/introduction/worker.ts`
|
|
334
|
+
|
|
335
|
+
```ts filename="reference/overview/introduction/worker.ts"
|
|
336
|
+
|
|
337
|
+
makeWorker({
|
|
338
|
+
schema,
|
|
339
|
+
sync: {
|
|
340
|
+
backend: makeWsSync({ url: `${location.origin}/sync` }),
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### `reference/overview/introduction/schema.ts`
|
|
346
|
+
|
|
347
|
+
```ts filename="reference/overview/introduction/schema.ts"
|
|
348
|
+
// schema.ts
|
|
349
|
+
|
|
350
|
+
// 1. Define events (the things that can happen in your app)
|
|
351
|
+
export const events = {
|
|
352
|
+
todoCreated: Events.synced({
|
|
353
|
+
name: 'v1.TodoCreated',
|
|
354
|
+
schema: Schema.Struct({
|
|
355
|
+
id: Schema.String,
|
|
356
|
+
text: Schema.String,
|
|
357
|
+
}),
|
|
358
|
+
}),
|
|
359
|
+
todoCompleted: Events.synced({
|
|
360
|
+
name: 'v1.TodoCompleted',
|
|
361
|
+
schema: Schema.Struct({ id: Schema.String }),
|
|
362
|
+
}),
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 2. Define SQLite tables (how to query your state)
|
|
366
|
+
export const tables = {
|
|
367
|
+
todos: State.SQLite.table({
|
|
368
|
+
name: 'todos',
|
|
369
|
+
columns: {
|
|
370
|
+
id: State.SQLite.text({ primaryKey: true }),
|
|
371
|
+
text: State.SQLite.text({ default: '' }),
|
|
372
|
+
completed: State.SQLite.boolean({ default: false }),
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 3. Define materializers (how to turn events into state)
|
|
378
|
+
const materializers = State.SQLite.materializers(events, {
|
|
379
|
+
'v1.TodoCreated': ({ id, text }) => tables.todos.insert({ id, text }),
|
|
380
|
+
'v1.TodoCompleted': ({ id }) => tables.todos.update({ completed: true }).where({ id }),
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
const state = State.SQLite.makeState({ tables, materializers })
|
|
384
|
+
export const schema = makeSchema({ events, state })
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
That's all the configuration needed—your app works offline automatically. When connectivity returns, LiveStore syncs pending events and reconciles state.
|
|
388
|
+
|
|
389
|
+
## When LiveStore fits
|
|
390
|
+
|
|
391
|
+
LiveStore works well for:
|
|
392
|
+
|
|
393
|
+
- **Productivity apps** (todo lists, note-taking, project management, ...)
|
|
394
|
+
- **Collaborative tools** with multi-player support
|
|
395
|
+
- **Apps with complex local state** that need SQL-level queries
|
|
396
|
+
- **Local-first** apps with offline support
|
|
397
|
+
- **Cross-platform apps** (web, mobile, desktop, server)
|
|
398
|
+
- ... or any combination of the above
|
|
399
|
+
|
|
400
|
+
LiveStore may not fit if:
|
|
401
|
+
|
|
402
|
+
- Your data must live on an existing server database (consider [ElectricSQL](https://electric-sql.com) or [Zero](https://zero.rocicorp.dev))
|
|
403
|
+
- You're building a traditional client-server app without offline needs
|
|
404
|
+
- You need unbounded data that won't fit in client memory
|
|
405
|
+
|
|
406
|
+
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).
|
|
407
|
+
|
|
408
|
+
## Next steps
|
|
409
|
+
|
|
410
|
+
- [Tutorial](/tutorial/0-welcome) — Step-by-step tutorial introducing the main concepts and workflows of LiveStore
|
|
411
|
+
- [Getting started with React](/getting-started/react-web) — Quickstart to set up a React app
|
|
412
|
+
- [How LiveStore works](/overview/how-livestore-works) — Deeper dive into the LiveStore architecture
|
|
413
|
+
- [Examples](/examples) — See LiveStore in action with TodoMVC, Linearlite, and more
|
|
@@ -1,5 +1,111 @@
|
|
|
1
1
|
# Why LiveStore?
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## The problem LiveStore solves
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Building modern apps with great UX means handling a lot of complexity:
|
|
6
|
+
|
|
7
|
+
- **State management**: Keeping UI in sync with data
|
|
8
|
+
- **Persistence**: Surviving refreshes and app restarts
|
|
9
|
+
- **Offline support**: Working without a network connection
|
|
10
|
+
- **Real-time sync**: Reflecting changes across devices instantly
|
|
11
|
+
- **Conflict resolution**: Handling concurrent edits gracefully
|
|
12
|
+
|
|
13
|
+
Traditionally, each of these concerns requires separate solutions—a state library, local storage, a sync service, optimistic update logic, retry queues. These pieces don't naturally fit together, leading to complex, fragile code and subtle bugs.
|
|
14
|
+
|
|
15
|
+
LiveStore provides a _unified architecture_ that handles all of these concerns through one coherent model: event sourcing.
|
|
16
|
+
|
|
17
|
+
As a positive side-effect, the simplicity of LiveStore's event-driven abstractions also leads to a superior DX compared to the traditional ways of dealing with state.
|
|
18
|
+
|
|
19
|
+
## What makes LiveStore different
|
|
20
|
+
|
|
21
|
+
### A real database, not just a cache
|
|
22
|
+
|
|
23
|
+
Most state management tools treat local data as a _cache_ of server state. This introduces complexity that's hard to manage due to the inherent intricacies of caching.
|
|
24
|
+
|
|
25
|
+
LiveStore flips this: your local SQLite database is the primary data source, and the server is just a sync target to which data is distributed automatically.
|
|
26
|
+
|
|
27
|
+
<SourceOfTruthDiagram class="my-8" />
|
|
28
|
+
|
|
29
|
+
This means:
|
|
30
|
+
- **No loading states for reads**—queries execute synchronously against local SQLite
|
|
31
|
+
- **Full SQL power**—joins, aggregations, complex filters, all instant
|
|
32
|
+
- **Offline by default**—your app works the same whether online or not
|
|
33
|
+
|
|
34
|
+
### Events as the source of truth
|
|
35
|
+
|
|
36
|
+
Instead of syncing mutable state (which leads to "who wins?" conflicts), LiveStore syncs an append-only log of events. This is fundamentally easier to reason about:
|
|
37
|
+
|
|
38
|
+
- Events merge naturally—you're combining histories, not reconciling states
|
|
39
|
+
- Every change is auditable—you have a complete record of what happened
|
|
40
|
+
- State is always rebuildable—replay events to reconstruct any point in time
|
|
41
|
+
|
|
42
|
+
### Sync that actually works
|
|
43
|
+
|
|
44
|
+
LiveStore includes a battle-tested sync engine that handles the hard problems:
|
|
45
|
+
|
|
46
|
+
- **Optimistic updates**: Changes appear instantly, sync happens in the background
|
|
47
|
+
- **Automatic offline queue**: Events committed offline sync when connectivity returns
|
|
48
|
+
- **Deterministic conflict resolution**: Same events always produce the same state
|
|
49
|
+
- **Pluggable backends**: Use Cloudflare, ElectricSQL, or build your own
|
|
50
|
+
|
|
51
|
+
### Built for demanding apps
|
|
52
|
+
|
|
53
|
+
LiveStore was developed as the foundation for [Overtone](https://overtone.pro), a professional music application requiring 120fps performance with complex, real-time collaborative state. It's designed for apps where performance and reliability aren't negotiable.
|
|
54
|
+
|
|
55
|
+
### Open-source: By developers, for developers
|
|
56
|
+
|
|
57
|
+
LiveStore is entirely open-source. There is no company behind it and it's only possible through its generous [sponsors](/sustainable-open-source/sponsoring).
|
|
58
|
+
|
|
59
|
+
## LiveStore vs. the alternatives
|
|
60
|
+
|
|
61
|
+
### vs. State management libraries (Redux, Zustand, MobX)
|
|
62
|
+
|
|
63
|
+
These solve state management but not persistence or sync. You'll need to add:
|
|
64
|
+
- Local storage or IndexedDB for persistence
|
|
65
|
+
- A sync layer for real-time updates
|
|
66
|
+
- Optimistic update logic
|
|
67
|
+
- Offline queue management
|
|
68
|
+
|
|
69
|
+
LiveStore handles all of this out of the box.
|
|
70
|
+
|
|
71
|
+
### vs. Backend-as-a-Service (Firebase, Supabase)
|
|
72
|
+
|
|
73
|
+
These provide sync but treat the server as the source of truth. The consequences are:
|
|
74
|
+
- Reads require network round-trips (or complex caching)
|
|
75
|
+
- Offline support is limited or requires significant extra work
|
|
76
|
+
- You're locked into their data model and pricing
|
|
77
|
+
|
|
78
|
+
LiveStore is client-first, works fully offline, and syncs via any backend you choose.
|
|
79
|
+
|
|
80
|
+
### vs. Other local-first solutions (ElectricSQL, Zero, PowerSync)
|
|
81
|
+
|
|
82
|
+
These are excellent if you have an existing Postgres database you want to sync to clients. LiveStore is better when:
|
|
83
|
+
- You're building a new app without legacy data
|
|
84
|
+
- You want event sourcing semantics (not just row-level sync)
|
|
85
|
+
- You need the flexibility to materialize state in different ways
|
|
86
|
+
|
|
87
|
+
See the [local-first landscape](https://localfirst.fm/landscape) for a comprehensive comparison.
|
|
88
|
+
|
|
89
|
+
## A great developer experience (DX)
|
|
90
|
+
|
|
91
|
+
LiveStore is designed to make the right thing easy:
|
|
92
|
+
|
|
93
|
+
- **Type-safe schema**: Your events, tables, and queries are fully typed
|
|
94
|
+
- **Reactive by default**: UI updates automatically when data changes
|
|
95
|
+
- **Powerful devtools**: Inspect state, browse events, time-travel debug
|
|
96
|
+
- **Cross-platform**: Same mental model on web, mobile, desktop, and server
|
|
97
|
+
|
|
98
|
+
## When to choose LiveStore
|
|
99
|
+
|
|
100
|
+
**Choose LiveStore if:**
|
|
101
|
+
- You're building a new app that should work offline
|
|
102
|
+
- You want a unified solution for state, persistence, and sync
|
|
103
|
+
- Your app has complex local state that benefits from SQL queries
|
|
104
|
+
- You value auditability and the ability to replay/debug state changes
|
|
105
|
+
|
|
106
|
+
**Consider alternatives if:**
|
|
107
|
+
- You need to sync with an existing server database
|
|
108
|
+
- Your data won't fit in client memory
|
|
109
|
+
- You're building a traditional request/response app without offline needs
|
|
110
|
+
|
|
111
|
+
Still unsure? Try the [evaluation exercise](/overview/when-livestore#evaluation-exercise) to model your app's events and see if it feels natural.
|