@livestore/livestore 0.4.0-dev.22 → 0.4.0-dev.23

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.
Files changed (207) hide show
  1. package/README.md +0 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/QueryCache.js +1 -1
  4. package/dist/QueryCache.js.map +1 -1
  5. package/dist/SqliteDbWrapper.d.ts +5 -5
  6. package/dist/SqliteDbWrapper.d.ts.map +1 -1
  7. package/dist/SqliteDbWrapper.js +8 -8
  8. package/dist/SqliteDbWrapper.js.map +1 -1
  9. package/dist/SqliteDbWrapper.test.js +2 -2
  10. package/dist/SqliteDbWrapper.test.js.map +1 -1
  11. package/dist/effect/LiveStore.d.ts +14 -7
  12. package/dist/effect/LiveStore.d.ts.map +1 -1
  13. package/dist/effect/LiveStore.js +0 -15
  14. package/dist/effect/LiveStore.js.map +1 -1
  15. package/dist/effect/LiveStore.test.d.ts +2 -0
  16. package/dist/effect/LiveStore.test.d.ts.map +1 -0
  17. package/dist/effect/LiveStore.test.js +42 -0
  18. package/dist/effect/LiveStore.test.js.map +1 -0
  19. package/dist/live-queries/base-class.d.ts +3 -3
  20. package/dist/live-queries/base-class.d.ts.map +1 -1
  21. package/dist/live-queries/base-class.js +2 -2
  22. package/dist/live-queries/base-class.js.map +1 -1
  23. package/dist/live-queries/client-document-get-query.d.ts +1 -1
  24. package/dist/live-queries/client-document-get-query.d.ts.map +1 -1
  25. package/dist/live-queries/client-document-get-query.js +1 -1
  26. package/dist/live-queries/client-document-get-query.js.map +1 -1
  27. package/dist/live-queries/computed.d.ts.map +1 -1
  28. package/dist/live-queries/computed.js +2 -2
  29. package/dist/live-queries/computed.js.map +1 -1
  30. package/dist/live-queries/db-query.js +14 -14
  31. package/dist/live-queries/db-query.js.map +1 -1
  32. package/dist/live-queries/db-query.test.js +2 -2
  33. package/dist/live-queries/db-query.test.js.map +1 -1
  34. package/dist/live-queries/signal.test.js +2 -2
  35. package/dist/live-queries/signal.test.js.map +1 -1
  36. package/dist/mod.d.ts +1 -1
  37. package/dist/mod.d.ts.map +1 -1
  38. package/dist/mod.js.map +1 -1
  39. package/dist/reactive.d.ts +9 -9
  40. package/dist/reactive.d.ts.map +1 -1
  41. package/dist/reactive.js +9 -26
  42. package/dist/reactive.js.map +1 -1
  43. package/dist/reactive.test.js +2 -2
  44. package/dist/reactive.test.js.map +1 -1
  45. package/dist/store/StoreRegistry.d.ts +30 -5
  46. package/dist/store/StoreRegistry.d.ts.map +1 -1
  47. package/dist/store/StoreRegistry.js +54 -31
  48. package/dist/store/StoreRegistry.js.map +1 -1
  49. package/dist/store/StoreRegistry.test.js +251 -250
  50. package/dist/store/StoreRegistry.test.js.map +1 -1
  51. package/dist/store/create-store.d.ts +6 -2
  52. package/dist/store/create-store.d.ts.map +1 -1
  53. package/dist/store/create-store.js +13 -7
  54. package/dist/store/create-store.js.map +1 -1
  55. package/dist/store/devtools.d.ts +1 -1
  56. package/dist/store/devtools.d.ts.map +1 -1
  57. package/dist/store/devtools.js +3 -3
  58. package/dist/store/devtools.js.map +1 -1
  59. package/dist/store/store-eventstream.test.js +2 -2
  60. package/dist/store/store-eventstream.test.js.map +1 -1
  61. package/dist/store/store-types.d.ts +70 -5
  62. package/dist/store/store-types.d.ts.map +1 -1
  63. package/dist/store/store-types.js.map +1 -1
  64. package/dist/store/store-types.test.js +1 -1
  65. package/dist/store/store-types.test.js.map +1 -1
  66. package/dist/store/store.d.ts +81 -2
  67. package/dist/store/store.d.ts.map +1 -1
  68. package/dist/store/store.js +128 -45
  69. package/dist/store/store.js.map +1 -1
  70. package/dist/utils/dev.js.map +1 -1
  71. package/dist/utils/stack-info.js +2 -2
  72. package/dist/utils/stack-info.js.map +1 -1
  73. package/dist/utils/tests/fixture.d.ts +1 -1
  74. package/dist/utils/tests/fixture.d.ts.map +1 -1
  75. package/dist/utils/tests/fixture.js.map +1 -1
  76. package/dist/utils/tests/otel.d.ts.map +1 -1
  77. package/dist/utils/tests/otel.js +5 -5
  78. package/dist/utils/tests/otel.js.map +1 -1
  79. package/package.json +58 -17
  80. package/src/QueryCache.ts +1 -1
  81. package/src/SqliteDbWrapper.test.ts +4 -2
  82. package/src/SqliteDbWrapper.ts +12 -11
  83. package/src/ambient.d.ts +0 -7
  84. package/src/effect/LiveStore.test.ts +61 -0
  85. package/src/effect/LiveStore.ts +17 -26
  86. package/src/live-queries/__snapshots__/db-query.test.ts.snap +336 -231
  87. package/src/live-queries/base-class.ts +7 -6
  88. package/src/live-queries/client-document-get-query.ts +4 -2
  89. package/src/live-queries/computed.ts +3 -2
  90. package/src/live-queries/db-query.test.ts +3 -2
  91. package/src/live-queries/db-query.ts +15 -15
  92. package/src/live-queries/signal.test.ts +3 -2
  93. package/src/mod.ts +1 -0
  94. package/src/reactive.test.ts +3 -2
  95. package/src/reactive.ts +22 -23
  96. package/src/store/StoreRegistry.test.ts +317 -293
  97. package/src/store/StoreRegistry.ts +63 -38
  98. package/src/store/create-store.ts +26 -11
  99. package/src/store/devtools.ts +5 -6
  100. package/src/store/store-eventstream.test.ts +4 -2
  101. package/src/store/store-types.test.ts +3 -1
  102. package/src/store/store-types.ts +47 -8
  103. package/src/store/store.ts +172 -55
  104. package/src/utils/dev.ts +2 -2
  105. package/src/utils/stack-info.ts +2 -2
  106. package/src/utils/tests/fixture.ts +2 -1
  107. package/src/utils/tests/otel.ts +8 -7
  108. package/docs/api/index.md +0 -3
  109. package/docs/building-with-livestore/complex-ui-state/index.md +0 -3
  110. package/docs/building-with-livestore/crud/index.md +0 -3
  111. package/docs/building-with-livestore/data-modeling/index.md +0 -30
  112. package/docs/building-with-livestore/debugging/index.md +0 -17
  113. package/docs/building-with-livestore/devtools/index.md +0 -79
  114. package/docs/building-with-livestore/events/index.md +0 -355
  115. package/docs/building-with-livestore/examples/ai-agent/index.md +0 -5
  116. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -885
  117. package/docs/building-with-livestore/examples/turnbased-game/index.md +0 -7
  118. package/docs/building-with-livestore/opentelemetry/index.md +0 -227
  119. package/docs/building-with-livestore/production-checklist/index.md +0 -5
  120. package/docs/building-with-livestore/reactivity-system/index.md +0 -202
  121. package/docs/building-with-livestore/rules-for-ai-agents/index.md +0 -9
  122. package/docs/building-with-livestore/state/materializers/index.md +0 -300
  123. package/docs/building-with-livestore/state/sql-queries/index.md +0 -94
  124. package/docs/building-with-livestore/state/sqlite/index.md +0 -45
  125. package/docs/building-with-livestore/state/sqlite-schema/index.md +0 -306
  126. package/docs/building-with-livestore/state/sqlite-schema-effect/index.md +0 -300
  127. package/docs/building-with-livestore/store/index.md +0 -625
  128. package/docs/building-with-livestore/syncing/index.md +0 -136
  129. package/docs/building-with-livestore/tools/cli/index.md +0 -177
  130. package/docs/building-with-livestore/tools/mcp/index.md +0 -187
  131. package/docs/examples/cloudflare-adapter/index.md +0 -44
  132. package/docs/examples/expo-adapter/index.md +0 -44
  133. package/docs/examples/index.md +0 -55
  134. package/docs/examples/node-adapter/index.md +0 -44
  135. package/docs/examples/web-adapter/index.md +0 -52
  136. package/docs/framework-integrations/custom-elements/index.md +0 -142
  137. package/docs/framework-integrations/react-integration/index.md +0 -937
  138. package/docs/framework-integrations/solid-integration/index.md +0 -293
  139. package/docs/framework-integrations/svelte-integration/index.md +0 -42
  140. package/docs/framework-integrations/vue-integration/index.md +0 -294
  141. package/docs/getting-started/expo/index.md +0 -882
  142. package/docs/getting-started/node/index.md +0 -115
  143. package/docs/getting-started/react-web/index.md +0 -626
  144. package/docs/getting-started/solid/index.md +0 -3
  145. package/docs/getting-started/vue/index.md +0 -471
  146. package/docs/index.md +0 -208
  147. package/docs/llms.txt +0 -146
  148. package/docs/misc/CODE_OF_CONDUCT/index.md +0 -133
  149. package/docs/misc/FAQ/index.md +0 -37
  150. package/docs/misc/community/index.md +0 -88
  151. package/docs/misc/credits/index.md +0 -14
  152. package/docs/misc/design-partners/index.md +0 -13
  153. package/docs/misc/package-management/index.md +0 -21
  154. package/docs/misc/performance/index.md +0 -25
  155. package/docs/misc/resources/index.md +0 -46
  156. package/docs/misc/state-of-the-project/index.md +0 -37
  157. package/docs/misc/troubleshooting/index.md +0 -82
  158. package/docs/overview/concepts/index.md +0 -78
  159. package/docs/overview/how-livestore-works/index.md +0 -56
  160. package/docs/overview/introduction/index.md +0 -413
  161. package/docs/overview/technology-comparison/index.md +0 -40
  162. package/docs/overview/when-livestore/index.md +0 -81
  163. package/docs/overview/why-livestore/index.md +0 -111
  164. package/docs/patterns/ai/index.md +0 -15
  165. package/docs/patterns/anonymous-user-transition/index.md +0 -10
  166. package/docs/patterns/app-evolution/index.md +0 -72
  167. package/docs/patterns/auth/index.md +0 -377
  168. package/docs/patterns/effect/index.md +0 -1505
  169. package/docs/patterns/encryption/index.md +0 -6
  170. package/docs/patterns/external-data/index.md +0 -5
  171. package/docs/patterns/file-management/index.md +0 -11
  172. package/docs/patterns/file-structure/index.md +0 -14
  173. package/docs/patterns/list-ordering/index.md +0 -369
  174. package/docs/patterns/offline/index.md +0 -32
  175. package/docs/patterns/orm/index.md +0 -18
  176. package/docs/patterns/presence/index.md +0 -11
  177. package/docs/patterns/rich-text-editing/index.md +0 -11
  178. package/docs/patterns/server-side-clients/index.md +0 -97
  179. package/docs/patterns/side-effects/index.md +0 -11
  180. package/docs/patterns/state-machines/index.md +0 -11
  181. package/docs/patterns/storybook/index.md +0 -209
  182. package/docs/patterns/undo-redo/index.md +0 -9
  183. package/docs/patterns/version-control/index.md +0 -8
  184. package/docs/platform-adapters/cloudflare-durable-object-adapter/index.md +0 -453
  185. package/docs/platform-adapters/electron-adapter/index.md +0 -15
  186. package/docs/platform-adapters/expo-adapter/index.md +0 -262
  187. package/docs/platform-adapters/node-adapter/index.md +0 -160
  188. package/docs/platform-adapters/tauri-adapter/index.md +0 -15
  189. package/docs/platform-adapters/web-adapter/index.md +0 -287
  190. package/docs/sustainable-open-source/contributing/docs/index.md +0 -94
  191. package/docs/sustainable-open-source/contributing/info/index.md +0 -63
  192. package/docs/sustainable-open-source/contributing/monorepo/index.md +0 -195
  193. package/docs/sustainable-open-source/sponsoring/index.md +0 -104
  194. package/docs/sync-providers/cloudflare/index.md +0 -773
  195. package/docs/sync-providers/custom/index.md +0 -65
  196. package/docs/sync-providers/electricsql/index.md +0 -159
  197. package/docs/sync-providers/s2/index.md +0 -230
  198. package/docs/tutorial/0-welcome/index.md +0 -48
  199. package/docs/tutorial/1-setup-starter-project/index.md +0 -105
  200. package/docs/tutorial/2-deploy-to-cloudflare/index.md +0 -195
  201. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +0 -530
  202. package/docs/tutorial/4-sync-data-via-cloudflare/index.md +0 -210
  203. package/docs/tutorial/5-expand-business-logic/index.md +0 -174
  204. package/docs/tutorial/6-persist-ui-state/index.md +0 -453
  205. package/docs/tutorial/7-next-steps/index.md +0 -22
  206. package/docs/understanding-livestore/design-decisions/index.md +0 -33
  207. package/docs/understanding-livestore/event-sourcing/index.md +0 -40
@@ -1,111 +0,0 @@
1
- # Why LiveStore?
2
-
3
- ## The problem LiveStore solves
4
-
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.
@@ -1,15 +0,0 @@
1
- # AI
2
-
3
- - LiveStore is a great fit for building AI applications.
4
- - Scenarios:
5
- - Local RAG (via sqlite-vec (see [feature request](https://github.com/livestorejs/livestore/issues/127)) + local LLM e.g. Gemini Nano embedded in Chrome)
6
- - Agentic applications
7
-
8
- - Event <> tool calls
9
- - Nice mapping
10
-
11
- ## Example
12
-
13
- ```ts
14
- // TODO (contribution welcome)
15
- ```
@@ -1,10 +0,0 @@
1
- # Anonymous user transition
2
-
3
- ## Basic idea
4
-
5
- - Locally choose a unique identifier for the user (e.g. via `crypto.randomUUID()`).
6
- - You might want to handle the very unlikely case that the identifier is not unique (collision) on the sync backend.
7
- - Persist this identifier locally (either via a separate LiveStore instance or via `localStorage`).
8
- - Use this identifier in the `storeId` for the user-related LiveStore instance.
9
- - Initially when the user is anonymous, the store won't be synced yet (i.e. no sync backend used in adapter).
10
- - 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.
@@ -1,72 +0,0 @@
1
- # App evolution
2
-
3
- When building an app with LiveStore, you'll need to keep some things in mind when evolving your app.
4
-
5
- ## Schema changes
6
-
7
- ### State schema changes
8
-
9
- Generally any kind of changes to your state schema (e.g. SQLite tables, ...) can be done at any time without any further considerations assuming the event materializer is updated to support the new schema.
10
-
11
- ### Event schema changes
12
-
13
- Event schema changes require a bit more consideration. Changes to the event schema should generally be done in a backwards-compatible way. See [Event schema evolution](/building-with-livestore/events#schema-evolution) for more details.
14
-
15
- ## Parallel different app versions
16
-
17
- In scenarios where you have multiple app versions rolled out in parallel (e.g. app version v3 with event schema v3 and app version v4 with event schema v4), you'll need to keep the following in mind:
18
-
19
- App instances running version 4 might commit events that are not yet supported by version 3. Your app needs to decide how to handle this scenario in one of the following ways:
20
-
21
- - Ignore unknown events
22
- - Cause an error in the app for unknown events
23
- - Handle events with a "catch all" event handler
24
- - Let app render a "app update required" screen. App can still be used in read-only mode.
25
- - ...
26
-
27
- LiveStore exposes a dedicated `unknownEventHandling` configuration on `makeSchema` so you can codify the desired behaviour instead of sprinkling ad-hoc checks across your app. The default is `'warn'`, which logs every unknown event and keeps processing.
28
-
29
- ## `reference/events/unknown-event-handling.ts`
30
-
31
- ```ts filename="reference/events/unknown-event-handling.ts"
32
-
33
- const tables = {
34
- todos: State.SQLite.table({
35
- name: 'todos',
36
- columns: {
37
- id: State.SQLite.text({ primaryKey: true }),
38
- text: State.SQLite.text(),
39
- },
40
- }),
41
- } as const
42
-
43
- const events = {
44
- todoCreated: Events.synced({
45
- name: 'v1.TodoCreated',
46
- schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
47
- }),
48
- } as const
49
-
50
- const materializers = State.SQLite.materializers(events, {
51
- [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
52
- tables.todos.insert({ id, text }),
53
- ),
54
- })
55
-
56
- const state = State.SQLite.makeState({ tables, materializers })
57
-
58
- // ---cut---
59
-
60
- const _schema = makeSchema({
61
- events,
62
- state,
63
- unknownEventHandling: {
64
- strategy: 'callback',
65
- onUnknownEvent: (event, error) => {
66
- console.warn('LiveStore saw an unknown event', { event, reason: error.reason })
67
- },
68
- },
69
- })
70
- ```
71
-
72
- Set the strategy to `'ignore'` to silently skip forward-only events, `'fail'` to stop immediately (useful during development), or `'callback'` to forward them to custom telemetry while continuing to replay the log.
@@ -1,377 +0,0 @@
1
- # Auth
2
-
3
- LiveStore doesn't include built-in authentication or authorization support, but you can implement it in your app's logic.
4
-
5
- ## Pass an auth payload to the sync backend
6
-
7
- Use the `syncPayload` store option to send a custom payload to your sync backend.
8
-
9
- ### Example
10
-
11
- The following example sends the authenticated user's JWT to the server.
12
-
13
- ## `patterns/auth/store-with-auth.tsx`
14
-
15
- ```tsx filename="patterns/auth/store-with-auth.tsx"
16
-
17
- const schema = {} as LiveStoreSchema
18
- const storeId = 'demo-store'
19
- const user = { jwt: 'user-token' }
20
- const adapter = makeInMemoryAdapter()
21
-
22
- // ---cut---
23
- const useAppStore = () =>
24
- useStore({
25
- storeId,
26
- schema,
27
- adapter,
28
- batchUpdates,
29
- syncPayload: {
30
- authToken: user.jwt, // Using a JWT
31
- },
32
- })
33
-
34
- export const App = () => {
35
- const [storeRegistry] = useState(() => new StoreRegistry())
36
- return (
37
- <Suspense fallback={<div>Loading...</div>}>
38
- <StoreRegistryProvider storeRegistry={storeRegistry}>
39
- <AppContent />
40
- </StoreRegistryProvider>
41
- </Suspense>
42
- )
43
- }
44
-
45
- const AppContent = () => {
46
- const _store = useAppStore()
47
- // Use the store in your components
48
- return <div>{/* Your app content */}</div>
49
- }
50
- ```
51
-
52
- On the sync server, validate the token and allow or reject the sync based on the result. See the following example:
53
-
54
- ## `patterns/auth/pass-auth-payload.ts`
55
-
56
- ```ts filename="patterns/auth/pass-auth-payload.ts"
57
-
58
- const JWT_SECRET = 'a-string-secret-at-least-256-bits-long'
59
-
60
- export class SyncBackendDO extends makeDurableObject({
61
- onPush: async (message) => {
62
- console.log('onPush', message.batch)
63
- },
64
- onPull: async (message) => {
65
- console.log('onPull', message)
66
- },
67
- }) {}
68
-
69
- export default makeWorker({
70
- syncBackendBinding: 'SYNC_BACKEND_DO',
71
- validatePayload: async (payload: any, context) => {
72
- const { storeId } = context
73
- const { authToken } = payload
74
-
75
- if (!authToken) {
76
- throw new Error('No auth token provided')
77
- }
78
-
79
- const user = await getUserFromToken(authToken)
80
-
81
- if (!user) {
82
- throw new Error('Invalid auth token')
83
- } else {
84
- // User is authenticated!
85
- console.log('Sync backend payload', JSON.stringify(user, null, 2))
86
- }
87
-
88
- // Check if token is expired
89
- if (payload.exp && payload.exp < Date.now() / 1000) {
90
- throw new Error('Token expired')
91
- }
92
-
93
- await checkUserAccess(user, storeId)
94
- },
95
- enableCORS: true,
96
- })
97
-
98
- async function getUserFromToken(token: string): Promise<jose.JWTPayload | undefined> {
99
- try {
100
- const { payload } = await jose.jwtVerify(token, new TextEncoder().encode(JWT_SECRET))
101
- return payload
102
- } catch (error) {
103
- console.log('⚠️ Error verifying token', error)
104
- }
105
- }
106
-
107
- async function checkUserAccess(payload: jose.JWTPayload, storeId: string): Promise<void> {
108
- // Check if user is authorized to access the store
109
- console.log('Checking access for store', storeId, 'with payload', payload)
110
- }
111
- ```
112
-
113
- The above example uses [`jose`](https://www.npmjs.com/package/jose), a popular JavaScript module that supports JWTs. It works across various runtimes, including Node.js, Cloudflare Workers, Deno, Bun, and others.
114
-
115
- The `validatePayload` function receives the `authToken`, checks if the payload exists, and verifies that it's valid and hasn't expired. If all checks pass, sync continues as normal. If any check fails, the server rejects the sync.
116
-
117
- The client app still works as expected, but saves data locally. If the user re-authenticates or refreshes the token later, LiveStore syncs any local changes made while the user was unauthenticated.
118
-
119
- ## Re-validate payload inside the Durable Object
120
-
121
- When you rely on `syncPayload`, treat it as untrusted input. Decode the token inside `validatePayload` to gate the connection, and then repeat the same verification inside the Durable Object before trusting per-push metadata.
122
-
123
- ## `patterns/auth/keep-payload-canonical.ts`
124
-
125
- ```ts filename="patterns/auth/keep-payload-canonical.ts"
126
-
127
- // ---cut---
128
- type SyncPayload = { authToken?: string; userId?: string }
129
-
130
- type AuthorizedSession = {
131
- authToken: string
132
- userId: string
133
- }
134
-
135
- const ensureAuthorized = (payload: unknown): AuthorizedSession => {
136
- if (payload === undefined || payload === null || typeof payload !== 'object') {
137
- throw new Error('Missing auth payload')
138
- }
139
-
140
- const { authToken, userId } = payload as SyncPayload
141
- if (!authToken) {
142
- throw new Error('Missing auth token')
143
- }
144
-
145
- const claims = verifyJwt(authToken)
146
- if (!claims.sub) {
147
- throw new Error('Token missing subject claim')
148
- }
149
-
150
- if (userId !== undefined && userId !== claims.sub) {
151
- throw new Error('Payload userId mismatch')
152
- }
153
-
154
- return { authToken, userId: claims.sub }
155
- }
156
-
157
- export default makeWorker({
158
- syncBackendBinding: 'SYNC_BACKEND_DO',
159
- validatePayload: (payload) => {
160
- ensureAuthorized(payload)
161
- },
162
- })
163
-
164
- export class SyncBackendDO extends makeDurableObject({
165
- onPush: async (message: SyncMessage.PushRequest, { payload }) => {
166
- const { userId } = ensureAuthorized(payload)
167
- await ensureTenantAccess(userId, message.batch)
168
- },
169
- }) {}
170
-
171
- const ensureTenantAccess = async (_userId: string, _batch: SyncMessage.PushRequest['batch']) => {
172
- // Replace with your application-specific access checks.
173
- }
174
- ```
175
-
176
- ### `patterns/auth/verify-jwt.ts`
177
-
178
- ```ts filename="patterns/auth/verify-jwt.ts"
179
- export type Claims = {
180
- sub?: string
181
- }
182
-
183
- export const verifyJwt = (token: string): Claims => {
184
- if (token.length === 0) {
185
- throw new Error('Missing token')
186
- }
187
-
188
- // Replace with real JWT verification (e.g. via `jose`)
189
- return { sub: token }
190
- }
191
- ```
192
-
193
- - `validatePayload` runs once per connection and rejects mismatched tokens before LiveStore upgrades to WebSocket.
194
- - `onPush` (and `onPull`, if you need it) must repeat the verification because the payload forwarded to the Durable Object is the original client input.
195
- - The HTTP transport does not forward payloads today; embed the necessary authorization context directly in the events or move those clients to WebSocket/DO-RPC if you must rely on shared payload metadata.
196
-
197
- You can extend `ensureAuthorized` to project additional claims, memoise verification per `authToken`, or enforce application-specific policies without changing LiveStore internals.
198
-
199
- ## Cookie-based authentication
200
-
201
- If you prefer cookie-based authentication (e.g., with [better-auth](https://www.better-auth.com/)), you can forward HTTP headers to your `onPush` and `onPull` callbacks using the `forwardHeaders` option.
202
-
203
- ### Why forward headers?
204
-
205
- Passing tokens in URL parameters (`syncPayload`) exposes them in browser history, server logs, and referrer headers. Cookie-based auth avoids these issues since cookies are sent automatically with each request and aren't logged in URLs.
206
-
207
- ### Example
208
-
209
- The following example forwards `Cookie` and `Authorization` headers to the Durable Object callbacks:
210
-
211
- ## `patterns/auth/cookie-auth.ts`
212
-
213
- ```ts filename="patterns/auth/cookie-auth.ts"
214
-
215
- export class SyncBackendDO extends makeDurableObject({
216
- // Forward Cookie and Authorization headers to onPush/onPull callbacks
217
- forwardHeaders: ['Cookie', 'Authorization'],
218
-
219
- onPush: async (message, context) => {
220
- const { storeId, headers } = context
221
-
222
- // Access forwarded headers in callbacks
223
- const cookie = headers?.get('cookie')
224
- const _authorization = headers?.get('authorization')
225
-
226
- if (cookie) {
227
- // Parse session from cookie (example with better-auth)
228
- const sessionToken = parseCookie(cookie, 'session_token')
229
- const session = await getSessionFromToken(sessionToken)
230
-
231
- if (!session) {
232
- throw new Error('Invalid session')
233
- }
234
-
235
- console.log('Push from user:', session.userId, 'store:', storeId)
236
- }
237
-
238
- console.log('onPush', message.batch)
239
- },
240
-
241
- onPull: async (message, context) => {
242
- const { storeId, headers } = context
243
-
244
- // Same header access in onPull
245
- const cookie = headers?.get('cookie')
246
-
247
- if (cookie) {
248
- const sessionToken = parseCookie(cookie, 'session_token')
249
- const session = await getSessionFromToken(sessionToken)
250
-
251
- if (!session) {
252
- throw new Error('Invalid session')
253
- }
254
-
255
- console.log('Pull from user:', session.userId, 'store:', storeId)
256
- }
257
-
258
- console.log('onPull', message)
259
- },
260
- }) {}
261
-
262
- export default makeWorker({
263
- syncBackendBinding: 'SYNC_BACKEND_DO',
264
- // Optional: validate at worker level using headers
265
- validatePayload: async (_payload, context) => {
266
- const { headers } = context
267
- const cookie = headers.get('cookie')
268
-
269
- if (cookie) {
270
- const sessionToken = parseCookie(cookie, 'session_token')
271
- const session = await getSessionFromToken(sessionToken)
272
-
273
- if (!session) {
274
- throw new Error('Unauthorized: Invalid session')
275
- }
276
- }
277
- },
278
- enableCORS: true,
279
- })
280
-
281
- // --- Helper functions (implement based on your auth library) ---
282
-
283
- function parseCookie(cookieHeader: string, name: string): string | undefined {
284
- const cookies = cookieHeader.split(';').map((c) => c.trim())
285
- for (const cookie of cookies) {
286
- const [key, value] = cookie.split('=')
287
- if (key === name) return value
288
- }
289
- return undefined
290
- }
291
-
292
- interface Session {
293
- userId: string
294
- email: string
295
- }
296
-
297
- async function getSessionFromToken(_token: string | undefined): Promise<Session | null> {
298
- // Implement session lookup using your auth library
299
- // Example with better-auth:
300
- // return await auth.api.getSession({ headers: { cookie: `session_token=${token}` } })
301
- return { userId: 'user-123', email: 'user@example.com' }
302
- }
303
- ```
304
-
305
- ### How it works
306
-
307
- 1. **Configure `forwardHeaders`** in `makeDurableObject()` to specify which headers to forward.
308
- 2. **Headers are stored** in the WebSocket attachment during connection upgrade, surviving hibernation.
309
- 3. **Access headers** via `context.headers` in `onPush` and `onPull` callbacks.
310
- 4. **Worker-level validation** can also access headers via `context.headers` in `validatePayload`.
311
-
312
- ### Custom header extraction
313
-
314
- For more control, pass a function to `forwardHeaders`:
315
-
316
- ```typescript
317
- export class SyncBackendDO extends makeDurableObject({
318
- forwardHeaders: (request) => ({
319
- 'x-user-id': request.headers.get('x-user-id') ?? '',
320
- 'x-session': request.headers.get('cookie')?.split('session=')[1]?.split(';')[0] ?? '',
321
- }),
322
- // ...
323
- }) {}
324
- ```
325
-
326
- ## Client identity vs user identity
327
-
328
- LiveStore's `clientId` identifies a client instance, while user identity is an application-level concern that must be modeled through your application's events and logic.
329
-
330
- ### Key points
331
- - `clientId`: Automatically managed by LiveStore, identifies a client instance
332
- - User identity: Managed by your application through events and syncPayload
333
-
334
- ### Using syncPayload for authentication
335
-
336
- The `syncPayload` is primarily intended for authentication purposes:
337
-
338
- ## `patterns/auth/store-with-auth.tsx`
339
-
340
- ```tsx filename="patterns/auth/store-with-auth.tsx"
341
-
342
- const schema = {} as LiveStoreSchema
343
- const storeId = 'demo-store'
344
- const user = { jwt: 'user-token' }
345
- const adapter = makeInMemoryAdapter()
346
-
347
- // ---cut---
348
- const useAppStore = () =>
349
- useStore({
350
- storeId,
351
- schema,
352
- adapter,
353
- batchUpdates,
354
- syncPayload: {
355
- authToken: user.jwt, // Using a JWT
356
- },
357
- })
358
-
359
- export const App = () => {
360
- const [storeRegistry] = useState(() => new StoreRegistry())
361
- return (
362
- <Suspense fallback={<div>Loading...</div>}>
363
- <StoreRegistryProvider storeRegistry={storeRegistry}>
364
- <AppContent />
365
- </StoreRegistryProvider>
366
- </Suspense>
367
- )
368
- }
369
-
370
- const AppContent = () => {
371
- const _store = useAppStore()
372
- // Use the store in your components
373
- return <div>{/* Your app content */}</div>
374
- }
375
- ```
376
-
377
- User identification and semantic data (like user IDs) should typically be handled through your event payloads and application state rather than relying solely on the sync payload.