NOTE: LiveStore is still in beta and releases can include breaking changes. See state of the project for more info. LiveStore is following a semver-like release strategy where breaking changes are released in minor versions before the 1.0 release.
- Removed redundant devenv package entries now owned by the task guard modules.
Installing v0.4.0: Make sure all LiveStore packages use the same version:
pnpm add @livestore/livestore @livestore/adapter-web @livestore/wa-sqlite @livestore/react # Or for Cloudflare pnpm add @livestore/livestore @livestore/adapter-cloudflare @livestore/sync-cf
- New Cloudflare adapter: Added the Workers/Durable Object adapter and rewrote the sync provider so LiveStore ships WebSocket, HTTP, and Durable Object RPC transports as first-party Cloudflare options (#528, #591).
- S2 sync backend: Map LiveStore's event log onto S2's durable stream store via
@livestore/sync-s2, unlocking scalable basins/streams, SSE live tails, and transport-safe batching for large sync workloads (#292, #709). - Schema-first tables: LiveStore now accepts Effect schema definitions as SQLite table definitions, removing duplicate column configuration in applications (#544).
- Cloudflare sync provider storage: Default storage is now Durable Object (DO) SQLite, with an explicit option to use D1 via a named binding. Examples and docs updated to the DO‑by‑default posture (see issue #266, #693).
- MCP support: LiveStore now ships a CLI with a first-class MCP server so automation flows can connect to instances, query data, and commit events using the bundled tools (#705).
- React multi-store API: The multi-store API is now the primary React integration, replacing
<LiveStoreProvider>with<StoreRegistryProvider>anduseStore()with store options. The new API supports multiple stores, preloading, and caching out of the box. See the React integration docs (#841).
-
store.shutdownAPI: The shutdown method now returns an Effect instead of a Promise. Useyield* store.shutdown()inside Effects orawait store.shutdownPromise()when a Promise is needed.// Before await store.shutdown() // After (Effect API) yield * store.shutdown() // Or use the Promise helper await store.shutdownPromise()
-
store.subscribecallback signature: The subscription callback is now passed as the second argument, with subscription options moved to an optional third argument object. Replace usages likestore.subscribe(query$, { onUpdate })withstore.subscribe(query$, onUpdate, options). -
QueryBuilder.first()behaviour:table.query.first()now returnsundefinedwhen no rows match. To keep the old behaviour, pass{ behaviour: "error" }, or supply a fallback.// Before: threw an error when no rows matched const user = table.query.first() // throws // After: returns undefined when no rows match const user = table.query.first() // returns undefined // To preserve old behaviour const strictUser = table.query.first({ behaviour: 'error' }) // Or provide a fallback value const fallbackUser = table.query.first({ behaviour: 'fallback', fallback: () => ({ id: 'default', name: 'Guest' }), })
-
Raw SQL event availability: The
livestore.RawSqlevent is no longer added automatically. Define it explicitly when needed (#469):import { Events, Schema } from '@livestore/livestore' const rawSqlEvent = Events.clientOnly({ name: 'livestore.RawSql', schema: Schema.Struct({ sql: Schema.String, bindValues: Schema.optional(Schema.Record(Schema.String, Schema.Any)), writeTables: Schema.optional(Schema.ReadonlySet(Schema.String)), }), })
-
wa-sqlite version alignment:
@livestore/wa-sqlitenow follows LiveStore's versioning scheme to keep adapters on matching releases.# Before: wa-sqlite had independent versioning pnpm add wa-sqlite@1.0.5 # After: wa-sqlite follows LiveStore versioning pnpm add @livestore/wa-sqlite@dev
This change affects projects that directly depend on wa-sqlite. Most users rely on it indirectly through LiveStore adapters and don't need to change anything.
-
Cloudflare sync provider storage selection: Removed implicit D1 auto‑selection and env‑based fallbacks. D1 must be selected explicitly via
storage: { _tag: 'd1', binding: 'DB' }on the sync Durable Object. The default is DO SQLite (see issue #266, #693).Before (implicit D1 when env.DB was present):
export class SyncBackendDO extends makeDurableObject({ // storage engine auto‑selected based on env }) {}
After (explicit binding for D1):
// wrangler.toml // [[d1_databases]] // binding = "DB" // database_name = "your-db" // database_id = "..." // code export class SyncBackendDO extends makeDurableObject({ storage: { _tag: 'd1', binding: 'DB' }, }) {}
To use the default DO SQLite, omit the
storageoption or pass{ _tag: 'do-sqlite' }. -
Restructured
LiveStoreEventandEventSequenceNumberAPIs: Types are now organized into symmetricGlobal,Client, andInputnamespaces that clarify the distinction between sync backend format, client format, and events without sequence numbers (#855):Old Name New Name LiveStoreEvent.AnyEncodedGlobalLiveStoreEvent.Global.EncodedLiveStoreEvent.AnyEncodedLiveStoreEvent.Client.EncodedLiveStoreEvent.AnyDecodedLiveStoreEvent.Client.DecodedLiveStoreEvent.PartialAnyEncodedLiveStoreEvent.Input.EncodedLiveStoreEvent.PartialAnyDecodedLiveStoreEvent.Input.DecodedLiveStoreEvent.EncodedWithMetaLiveStoreEvent.Client.EncodedWithMetaEventSequenceNumber.GlobalEventSequenceNumberEventSequenceNumber.GlobalEventSequenceNumber.globalEventSequenceNumberEventSequenceNumber.Global.makeEventSequenceNumber.localEventSequenceNumberEventSequenceNumber.Client.makeEventSequenceNumber.clientDefaultEventSequenceNumber.Client.DEFAULTEventSequenceNumber.rebaseGenerationDefaultEventSequenceNumber.REBASE_GENERATION_DEFAULTLiveStoreEvent.EventDefPartialSchemaLiveStoreEvent.EventDefInputSchemaLiveStoreEvent.makeEventDefPartialSchemaLiveStoreEvent.makeEventDefInputSchema// Before import { LiveStoreEvent, EventSequenceNumber } from '@livestore/livestore' const event: LiveStoreEvent.AnyEncoded = { ... } const globalSeq: EventSequenceNumber.GlobalEventSequenceNumber = 1 // After import { LiveStoreEvent, EventSequenceNumber } from '@livestore/livestore' const event: LiveStoreEvent.Client.Encoded = { ... } const globalSeq: EventSequenceNumber.Global = 1
-
Error class rename:
UnexpectedErrorhas been renamed toUnknownErrorfor better semantic clarity and consistency with Effect ecosystem naming conventions. The previous name conflicted with Effect's terminology where "unexpected errors" refer to defects, while this error type represents errors of unknown type from external libraries or infrastructure failures (See PR #823).Update all references:
- Class name:
UnexpectedError→UnknownError - Error tag:
'LiveStore.UnexpectedError'→'UnknownError' - Static methods:
mapToUnexpectedError*→mapToUnknownError* - Related type:
MergeResultUnexpectedError→MergeResultUnknownError
// Before import { UnexpectedError } from '@livestore/common' effect.pipe(UnexpectedError.mapToUnexpectedError) // After import { UnknownError } from '@livestore/common' effect.pipe(UnknownError.mapToUnknownError)
- Class name:
-
React integration API: The multi-store API is now the primary React integration, replacing
<LiveStoreProvider>and the olduseStore(). The new API usesStoreRegistry,<StoreRegistryProvider>, anduseStore()with store options. See the React integration docs for full details (#841).Before After <LiveStoreProvider schema={...} adapter={...}><StoreRegistryProvider storeRegistry={...}>+storeOptions({ ... })const { store } = useStore()const store = useStore({ ... })useQuery(query$)store.useQuery(query$)// Before import { LiveStoreProvider, useStore, useQuery } from '@livestore/react' const App = () => ( <LiveStoreProvider schema={schema} adapter={adapter} batchUpdates={batchUpdates}> <MyComponent /> </LiveStoreProvider> ) const MyComponent = () => { const { store } = useStore() const todos = useQuery(visibleTodos$) // ... } // After import { StoreRegistry } from '@livestore/livestore' import { StoreRegistryProvider, useStore } from '@livestore/react' const useAppStore = () => useStore({ storeId: 'app-root', schema, adapter, batchUpdates, }) const App = () => { const [storeRegistry] = useState(() => new StoreRegistry()) return ( <Suspense fallback={<div>Loading...</div>}> <StoreRegistryProvider storeRegistry={storeRegistry}> <MyComponent /> </StoreRegistryProvider> </Suspense> ) } const MyComponent = () => { const store = useAppStore() const todos = store.useQuery(visibleTodos$) // ... }
-
Removed top-level React hook exports:
useQuery,useQueryRef, anduseClientDocumentare no longer exported at the top level from@livestore/react. Use the store methods instead (#946):// Before import { useQuery, useClientDocument } from '@livestore/react' const todos = useQuery(query$) const [state, setState] = useClientDocument(table) // After import { useStore } from '@livestore/react' const store = useStore(storeOptions) // or via a custom hook wrapping useStore() (e.g. useAppStore()) const todos = store.useQuery(query$) const [state, setState] = store.useClientDocument(table)
Type exports (
UseClientDocumentResult,Dispatch,SetStateAction, etc.) remain available. -
S2 proxy helper signature changes: The
getSSEHeadersandgetPushHeadersfunctions in@livestore/sync-s2/s2-proxy-helpersnow accept anS2Configobject instead of a token string. This enables s2-lite support via the newliteflag which adds theS2-Basinheader for self-hosted S2 deployments (#978).// Before import * as S2Helpers from '@livestore/sync-s2/s2-proxy-helpers' const headers = S2Helpers.getSSEHeaders(token) const pushHeaders = S2Helpers.getPushHeaders(token) // After const config: S2Helpers.S2Config = { basin: 'my-basin', token: 'my-token' } const headers = S2Helpers.getSSEHeaders(config) const pushHeaders = S2Helpers.getPushHeaders(config) // For s2-lite (self-hosted), add the lite flag: const liteConfig: S2Helpers.S2Config = { basin: 'my-basin', token: 'unused', accountBase: 'http://localhost:4566/v1', basinBase: 'http://localhost:4566/v1', lite: true, // Adds S2-Basin header for s2-lite routing }
LiveStore now runs natively on Cloudflare Workers through the @livestore/adapter-cloudflare package. Durable Objects handle coordination and D1 provides persistence, enabling globally distributed applications with local-first behaviour. The adapter provides:
- Stateful Durable Objects for LiveStore instances
- D1 database integration for event persistence
- Hibernate-friendly architecture to minimise compute costs
- Direct integration with the Cloudflare sync provider
See the Cloudflare adapter documentation for setup details and deployment guidance.
- Cloudflare adapter: Added a development-only reset persistence option to clear Durable Object state (#664).
- Node and Expo adapters: Added development-only reset persistence options to clear local state (#654).
- Web adapter: Archive development state databases with bounded retention to avoid OPFS exhaustion (#649, thanks @IGassmann).
The @livestore/sync-cf package has been rewritten to offer three first-party transports—WebSocket, HTTP, and Durable Object RPC—so Cloudflare deployments can choose the right balance of latency and infrastructure support:
- WebSocket transport: Bidirectional real-time communication with automatic reconnection
- HTTP transport: Request/response sync with polling for environments that can’t keep WebSocket connections open
- Durable Object RPC: Direct Durable Object calls that avoid network overhead entirely
Key improvements include streaming pull operations (faster initial sync), a two-phase sync (bulk transfer followed by real-time updates), improved error recovery, and comprehensive test coverage.
-
Storage engine configuration: Default storage is DO SQLite; configure D1 explicitly with
storage: { _tag: 'd1', binding: '<binding>' }. Removed env‑based fallback detection. Docs include a “Storage engines” section, and examples default to DO storage (see issue #266). -
DO SQLite insert batching: Adjusted insert chunk size to stay under parameter limits for large batches.
-
WebSocket transport: Introduced message chunking limits to stay under platform constraints (#687).
-
Reliability: Retry and backoff on push errors, restart push on advance, and add regression tests (#639).
-
Resilience: Improve sync provider robustness and align test helpers for CI and local development (#682, #646).
-
Header forwarding: Added
forwardHeadersoption tomakeDurableObject()for cookie-based authentication. Headers are stored in WebSocket attachments to survive hibernation and accessible viacontext.headersinonPush/onPullcallbacks (#929). -
Backend reset detection: LiveStore now detects when a sync backend has been reset and handles it based on the
onBackendIdMismatchoption inSyncOptions. Default behaviour ('reset') clears local storage and shuts down so the app can restart with fresh data. Alternative modes include'shutdown'(shut down without clearing) and'ignore'(continue with stale data). See Backend Reset Detection docs (#980).
LiveStore now ships @livestore/sync-s2, a first-party integration with S2—the stream store that exposes basins and append-only streams over HTTP and SSE. LiveStore maps each storeId onto its own S2 stream while keeping LiveStore's logical sequencing inside the payload, so teams gain provider-managed durability, retention policies, and elastic fan-out without retooling their event model (#292). The provider still expects an authenticated proxy that provisions basins/streams, forwards LiveStore pushes and pulls, and translates S2 cursors back into LiveStore metadata.
- Stream primitives: Helper utilities (
ensureBasin(),ensureStream(),makeS2StreamName()) manage S2 provisioning and naming so apps can wire up a single/api/s2entry point without manual HTTP plumbing (#292). - Live pull over SSE: The client understands S2's
batch,ping, anderrorSSE events, keeping live cursors in sync while avoiding dropped connections and manual tail loops (#292). - Transport-safe batching: Append helpers respect S2's 1 MiB / 1000-record limits, preventing 413 responses while you stream large batches into managed storage (#709).
- s2-lite support: Added support for s2-lite, the open-source self-hosted S2. Set
lite: trueinS2Configto enable header-based basin routing. CI tests now run against s2-lite, removing the dependency on hosted S2 credentials (#978).
See the S2 sync provider docs for full deployment guidance and operational notes.
-
OpenTelemetry no-op tracing: SQLite initialization and live queries now measure debug timing independently of tracer internals, preventing crashes when no OpenTelemetry SDK provider is registered (#1448).
-
Event log lookup optimization: Improved event log lookup performance for large unsynced logs, speeding startup time (#1012).
-
DevTools protocol versioning: The app and DevTools now exchange an explicit protocol version during handshake, decoupling DevTools runtime compatibility from package versions. Newer or older DevTools builds connect cleanly to any LiveStore runtime that speaks the same protocol (#1232).
-
Unknown event handling: Schemas now ship an
unknownEventHandlingconfiguration so older clients can warn, ignore, fail, or forward telemetry when they see future events while keeping the eventlog intact (#353). -
Schema-first tables: LiveStore now accepts Effect schema definitions as SQLite table inputs, keeping type information and stored schema in the same place. For example:
// Define your schema const Recipe = Schema.Struct({ id: Schema.String.pipe(State.SQLite.withPrimaryKey), name: Schema.String, createdAt: Schema.String.pipe(State.SQLite.withDefault(() => 'CURRENT_TIMESTAMP')), }) // Create table with automatic column inference const recipes = State.SQLite.table({ name: 'recipes', schema: Recipe, })
This keeps the schema as a single source of truth, enforces types at compile time, and removes duplicate column definitions.
-
Materializer hash checks: Development builds compute hashes for materializer output and raise
MaterializerHashMismatchErrorwhen handlers diverge, catching non-pure implementations before they reach production.// This triggers warnings in development const materializers = State.SQLite.materializers(events, { todoCreated: (payload) => { const id = nanoid() // Non-pure: different ID each call const timestamp = Date.now() // Non-pure: uses external state return todos.insert({ id, ...payload, createdAt: timestamp, }) }, // Pure materializer - no warnings userRegistered: (payload) => users.insert(payload), })
Pure materializers ensure deterministic replay during sync, improve test reliability, and make debugging predictable.
-
Event deprecation support: Mark entire events or individual fields as deprecated to guide schema evolution. When deprecated events are committed or deprecated fields have values, a warning is logged via Effect's logging system to help teams migrate away from legacy patterns (#956).
import { Events } from '@livestore/livestore' import { Schema } from 'effect' import { deprecated } from '@livestore/common/schema' // Field-level deprecation const todoUpdated = Events.synced({ name: 'v1.TodoUpdated', schema: Schema.Struct({ id: Schema.String, title: Schema.optional(Schema.String).pipe(deprecated("Use 'text' instead")), text: Schema.optional(Schema.String), }), }) // Event-level deprecation const todoRenamed = Events.synced({ name: 'v1.TodoRenamed', schema: Schema.Struct({ id: Schema.String, name: Schema.String }), deprecated: "Use 'v1.TodoUpdated' instead", })
-
Per-store
unusedCacheTimeinStoreRegistry: Each store managed by aStoreRegistrycan now specify its ownunusedCacheTimeviastoreOptions(), overriding the registry-level default. Short-lived ephemeral stores can be disposed quickly while persistent stores stay cached longer (#917). -
Store:
store.networkStatusnow surfaces sync backend connectivity so apps can read the latest status or subscribe directly; the signal is no longer re-exposed on client sessions (#394). -
LiveStoreSchema.Anytype alias simplifies schema composition across adapters. -
Query builder const assertions improve type inference, and
store.subscribe()now accepts query builders (#371, thanks @rgbkrk). -
Store.subscribe async iteration: The async iterator overload now exposes a first-class
AsyncIterablesofor awaitloops work without manual casts, and the new exportedQueryabletype documents the accepted inputs (#736). -
Queryable type export:
packages/@livestore/livestorenow re-exportsQueryable<TResult>so shared utilities and framework adapters can describe the exact shapes accepted bystore.subscribeandsubscribeStream(#736). -
Store operations after shutdown are rejected with a descriptive
UnknownError. Shutdown now returns an Effect (see breaking changes). -
Exact optional property types are enabled, surfacing missing optional handling at compile time (#600).
-
Effect
EqualandHashimplementations forLiveQueryDefandSignalDefimprove comparisons. -
Sync payload and store ID are exposed to
onPull/onPushhandlers (#451). -
Materializers receive each event's
clientId, simplifying multi-client workflows (#574). -
React peer dependency relaxed from exact to caret range for smoother upgrades (#621).
-
Effect integration: Added
Store.Tag(schema, storeId)API for idiomatic Effect usage. Returns a yieldableContext.Tagwith static accessors (query,commit,use) and alayer()factory method. The previousmakeStoreContext()andLiveStoreContextLayer()APIs are now deprecated:import { Store } from '@livestore/livestore/effect' // Before (deprecated) const MainStoreContext = makeStoreContext<typeof schema>()('main') const MainStore = MainStoreContext.Tag const MainStoreLayer = MainStoreContext.Layer({ schema, adapter, ... }) // After const TodoStore = Store.Tag(schema, 'todos') const TodoStoreLayer = TodoStore.layer({ adapter, batchUpdates }) // Use in Effect code - yield directly or use static accessors Effect.gen(function* () { const { store } = yield* TodoStore const todos = yield* TodoStore.query(tables.todos.select()) yield* TodoStore.commit(events.todoCreated({ id: '1', text: 'Buy milk' })) })
- Fix client document schema migration with optimistic decoding (#588)
- Fix race condition in schema migration initialization (#566)
- Fix handling of optional fields without defaults in client documents (#487)
- Fix query builder method order to preserve where clauses (#586)
- Fix Symbol values in QueryCache key generation
- Fix SQLite query builder clause order so LIMIT precedes OFFSET, preventing syntax errors (#882)
- Fix
useQueryreturning stale results after aStoreis disposed and recreated with the same(storeId, clientId, sessionId). TheuseRcResourcecache is now scoped perStoreinstance via aWeakMap, so a replaced store gets a fresh bucket and previously cachedLiveQueryinstances become GC-eligible (#1186, #1241).
- Fix SQLite connections not closed on store disposal, preventing database reset after file deletion in the Expo adapter (#1171). Thanks @OrkhanAlikhanov for the detailed repro.
- Fix in-memory SQLite database connection handling in Expo adapter
- Fix OPFS file pool capacity exhaustion from old state databases (#569)
- Upgrade wa-sqlite to SQLite 3.50.4 (#581)
- WAL snapshot guard:
@livestore/sqlite-wasmnow aborts WAL-mode snapshot imports with an explicitSqliteError, preventing silent corruption when loading backups (#694). - Fix
changeset_applycrash during rebase rollback: The conflict callback was coerced to a null pointer when passed to WASM, causingRuntimeError: function signature mismatchduring concurrent multi-tab edits. Now wired through the C adapter relay pattern matching other callback APIs.xConflictandxFilterare explicit parameters on the public API (#998). Thanks, @slashv for the detailed reproduction and @acusti for the initial investigation.
-
Fix
useStorehook-order violation in React strict mode by moving theretaineffect after theReact.use()suspension point (#1181) -
Fix background push fiber dying silently on non-
RejectedPushErrorfailures inClientSessionSyncProcessor, leaving sessions unable to push (#1133) -
Fix
toGlobal()leaking a debugtoJSONmethod onto the returnedGlobal.Encodedobject, causingJSON.stringifyto produce string seqNums instead of integers in custom sync backends (#1165). Thanks @OrkhanAlikhanov for diagnosing the root cause. -
Fix correct type assertion in withLock function
-
Fix finalizers execution order (#450)
-
Ensure large batches no longer leave follower sessions behind by reconciling leader/follower heads correctly (#362)
-
Detect sync backend identity mismatches after Cloudflare state resets and surface an actionable error instead of silent failure (#389)
-
Stop advancing the backend head when materializers crash so subsequent boots no longer fail (#409)
-
Prevent
store.subscribereentrancy crashes by restoring the reactive debug context after nested commits (#577, #656) -
Fix
subscribewithskipInitialRunto properly register reactive dependencies while suppressing the initial callback (#847) -
Fix event equality check failing when args key order differs, which caused duplicate events when syncing with backends that reorder JSON keys (e.g. PostgreSQL
jsonb) (#1160) -
Fix event equality check failing when args use
Schema.UndefinedOror looseSchema.optionaland the field is omitted at commit time, which caused the sync merge to falsely take the rebase path and triggerMaterializerHashMismatchErrorfor state-dependent materializers (#1217)
- Fix TypeScript build issues and examples restructuring
- Fix TypeScript erasableSyntaxOnly compatibility issues (#459)
table.insert()now correctly omits nullable fields: Schema-derived table definitions previously required all fields ininsert()calls. Nullable columns (e.g.S.NullOr) are now correctly omittable, matching SQL semantics where nullable columns implicitly default toNULL(#1117).
- New example: CF Chat: A Cloudflare Durable Objects chat example demonstrates WebSocket sync, reactive message handling, and bot integrations across client React components and Durable Object services.
- Cloudflare examples now default to DO SQLite storage. D1 usage is documented via an explicit binding and a one‑line
storageoption in code. - Cloudflare Workers deployments:
mono examples deploynow provisions Worker targets so DO-backed demos stay current across prod and dev environments (#690, #735). - Add Netlify dev deployments for examples to simplify testing (#684).
- Svelte integration docs: Added the Svelte framework guide plus the Svelte TodoMVC example so
@livestore/svelteis documented alongside React and Solid. - Use Twoslash for select getting started snippets in docs (#658).
- TanStack Start examples:
web-linearlite,web-todomvc-sync-electric, andweb-todomvc-sync-s2now run on TanStack Start with Vite 7 compatibility fixes and Cloudflare runtime flags (#747). - Docs for coding agents: Documentation now serves agent-optimised Markdown so automations get concise answers without burning unnecessary tokens (#715).
- TypeScript-validated snippets: Most examples are now type checked through the Twoslash pipeline enabling in-docs intellisense (#715).
- LiveStore CLI for project scaffolding (experimental preview, not production-ready). Generated projects now link to the
mainbranch in the source LiveStore repository (#1206), and the CLI usesGITHUB_TOKENorGH_TOKENfor example downloads when available so rate-limited unauthenticated fetches are no longer the default (#1201).
- Effect updated to 3.17.14
- React updated to 19.1.1
- Vite updated to 7.1.7
- TypeScript 5.9.2 compatibility
Updates in this section are primarily relevant to maintainers and contributors. They cover infrastructure, tooling, and other non-user-facing work that supports the release.
- Encapsulated Store internals behind
StoreInternalsSymbol(movedboot,syncProcessor,effectContext,tableRefs,otel,sqliteDbWrapper,clientSession,activeQueries,reactivityGraph,isShutdown), reducing public surface and clarifying API boundaries (#814).
- Comprehensive sync provider test suite with property-based testing (#386)
- Node.js sync test infrastructure with Wrangler dev server integration (#594)
- Parallel CI test execution reducing test time significantly (#523)
- Cloudflare sync provider tests run against both storage engines (D1 and DO SQLite) using separate wrangler configs.
- Strict peer dep composition: Added
@effect/vitesttoutilsEffectPeerDepsand@livestore/peer-deps, and deduplicated the peer-deps package to derive its dependency list from the canonicalutilsEffectPeerDepssource (#1107). - Hosted example link validation: Maintainers now have a shared deployment metadata source and
mono examples validate-linkscheck so docs and example deployments can catch stale first-party demo URLs before publishing (#1244). - Chrome DevTools extension assets restored: Restored
qrcode-generator2.0.4 in@livestore/utilsand included the Chrome DevTools extension assets in the release artifact flow so the published DevTools package contains the Chrome extension build alongside the Vite plugin (#1215). - Migration from ESLint to Biome for improved performance (#447)
- Automated dependency management with Renovate
- Pre-commit hooks via Husky (#522)
- Comprehensive dependency update script (#516)
- Add GitHub issue templates to improve issue quality (#602)
- Reworked the documentation tooling so maintainers continuously publish token-efficient, TypeScript-backed snippets that stay reliable for coding agents (#715)
- Snapshot release confirmation prompt: The
mono release snapshotcommand now prompts for confirmation before publishing. Pass--yesto skip the prompt in scripts and CI. The prompt is also auto-skipped whenCIis set (#1049). - Prod docs deploy phase split: The stable-release docs deploy is now split into six independently-timed phases (snippets, diagrams, astro, upload, verify, purge), each wrapped in an OS-level
timeout(1)+ heartbeat. This caps orphan Chromium children from the tldraw renderer at the OS boundary so a single hang no longer blocks the post-publish release flow. A newdeploy-prod.ymlworkflow lets operators re-dispatch a single failing target (gh workflow run deploy-prod.yml -f target=docs) without re-running the entire publish chain (#1279).
The wa-sqlite WebAssembly SQLite implementation has been integrated directly into the LiveStore monorepo as a git subtree under packages/@livestore/wa-sqlite. This change provides several benefits:
- Direct control over SQLite builds and customizations for LiveStore's needs
- Simplified dependency management and version alignment
- Ability to apply LiveStore-specific patches and optimizations
- Reduced external dependency risks and improved build reproducibility
Key changes:
- Integrated wa-sqlite as git subtree, replacing external npm dependency (#582)
- Ported build scripts and test infrastructure to LiveStore monorepo (#572)
- Updated to SQLite 3.50.4 with LiveStore-optimized configuration (#581)
- Fixed test setup issues and improved reliability (#583)
This integration lays the foundation for future SQLite optimizations specific to LiveStore's event-sourcing and sync requirements.
For remaining v0.4.0 work and known issues, see the v0.4.0 milestone on GitHub.
Open issues:
- Other tabs lag behind noticeably when committing large batches of events (#304)
- Vite DevTools consistently loses app connection (#331)
- Sync state memory leak: Unbounded pending events accumulation when no sync backend is used (#360)
- Type Annotations on schemas not portable (#383)
- store.shutdown() doesn't wait for pending writes to complete, causing data loss (#416)
- Consider mechanism to reject events on sync (#404)
- [Devtools] Clicking on session doesn't work (#474)
- Fix: Rolling back empty materializers currently fails
-
New sync implementation (based on git-like push/pull semantics)
- See Syncing docs page for more details
sync-cfbackend: More reliable websocket connection handling- Configurable sync semantics when app starts (either skip initial sync or block with timeout)
-
New: Node adapter
@livestore/adapter-node(experimental)- Note: Currently uses the
@livestore/sqlite-wasmbuild but the plan is to move to a native SQLite build in the future to improve performance and reduce bundle size. - Still lacks a few devtools-related flows (e.g. graceful import/reset)
- Note: Currently uses the
-
New:
@livestore/sync-electricbackend (experimental)- See docs page for more details
-
New:
@livestore/adapter-exponow supports syncing (requires Expo 53 or later):const adapter = makePersistedAdapter({ sync: { backend: makeWsSync({ url: `https://...` }) }, })
-
New: Solid integration
@livestore/solid(experimental)- Still very early stage and probably lacks some features. Feedback wanted!
- Thank you to @kulshekhar for the initial implementation! (See PR #225)
- There are is still a lot of work to be done - contributions welcome!
-
Breaking: Renamed adapter packages:
@livestore/webnow is@livestore/adapter-web@livestore/exponow is@livestore/adapter-expo
-
Breaking: Removed
@livestore/db-schemapackage and moved to@livestore/common/schema -
Breaking: Renamed
store.mutatetostore.commit- Reason: Make it more clear that committing mutations is also syncing them across other clients
-
Breaking: Adjusted schema API
- The new API aims to separate the schema into state and events
- Mutations are now split up into event definitions and materializer functions
Before:
// mutations.ts import { defineMutation, Schema } from '@livestore/livestore' // Mutations are now split up into event definitions and materializer functions export const todoCreated = defineMutation( 'todoCreated', Schema.Struct({ id: DbSchema.text(), text: DbSchema.text(), }), sql`INSERT INTO todos (id, text) VALUES (${id}, ${text})`, ) // schema.ts import { DbSchema, makeSchema } from '@livestore/livestore' import * as mutations from './mutations.js' const todos = DbSchema.table('todos', { id: DbSchema.text({ primaryKey: true }), text: DbSchema.text(), }) const uiState = DbSchema.table( 'uiState', { id: DbSchema.text({ primaryKey: true }), newTodoText: DbSchema.text(), filter: DbSchema.text({}), }, { derivedMutations: { clientOnly: true }, }, ) const tables = { todos, uiState } const schema = makeSchema({ tables, mutations })
After:
// events.ts import { Events, Schema } from '@livestore/livestore' export const todoCreated = Events.synced({ name: 'todoCreated', schema: Schema.Struct({ id: Schema.String, text: Schema.String }), }) // schema.ts import { makeSchema, Schema, State } from '@livestore/livestore' import * as events from './events.js' const todos = State.SQLite.table({ name: 'todos', columns: { id: State.SQLite.text({ primaryKey: true }), text: State.SQLite.text(), }, }) // tables with `deriveMutations` are now called `clientDocuments` const uiState = State.SQLite.clientDocument({ name: 'uiState', schema: Schema.Struct({ newTodoText: Schema.String, filter: Schema.String, }), }) const tables = { todos, uiState } // Materalizers let you materialize events into the state const materializers = State.SQLite.materializers(events, { 'v1.TodoCreated': ({ id, text }) => todos.insert({ id, text }), }) // Currently SQLite is the only supported state implementation but there might be more in the future (e.g. pure in-memory JS, DuckDB, ...) const state = State.SQLite.makeState({ tables, materializers }) // Schema is now more clearly separated into state and events const schema = makeSchema({ state, events })
-
Breaking
@livestore/react: RemoveduseScopedQueryin favour ofuseQuery. Migration example:// before const query$ = useScopedQuery(() => queryDb(tables.issues.query.where({ id: issueId }).first()), ['issue', issueId]) // after const query$ = useQuery( queryDb(tables.issues.query.where({ id: issueId }).first(), { deps: `issue-${issueId}`, }), )
-
Breaking
@livestore/adapter-web: RenamedmakeAdaptertomakePersistedAdapter -
Breaking
@livestore/adapter-expo: RenamedmakeAdaptertomakePersistedAdapter -
Breaking: Renamed
localOnlytoclientOnlyin table/mutation definitions. -
Breaking: Renamed
makeBackendtobackendin sync options. -
Breaking
@livestore/react:useClientDocumentnow only works with for tables with client-only derived mutations. -
Breaking: Instead of calling
query$.run()/query$.runAndDestroy(), please usestore.query(query$)instead. -
Breaking: Removed
store.__executefromStore. -
Breaking: Removed
globalReactivityGraphand explicit passing ofreactivityGraphto queries. -
Breaking: Removed
persistedoption fromstore.commit. This will be superceded by eventlog compaction in the future. -
Breaking: The new syncing implementation required some changes to the storage format. The
liveStoreStorageFormatVersionhas been bumped to3which will create new database files. -
Breaking: Moved
queryGraphQLto@livestore/graphqland thus removinggraphqlfrom peer dependencies of@livestore/livestore. -
Moved dev helper methods from e.g.
store.__devDownloadDb()tostore._dev.downloadDb() -
Breaking
@livestore/sync-cf: RenamedmakeWsSynctomakeWsSync
-
Added support for write queries in the query builder
table.query.insert({ id: '123', name: 'Alice' }) table.query.insert({ id: '123', name: 'Alice' }).onConflict('id', 'ignore') table.query.insert({ id: '123', name: 'Alice' }).returning('id') table.query.update({ name: 'Bob' }).where({ id: '123' }) table.query.delete().where({ id: '123' })
-
Introduced
@livestore/peer-depspackage to simplify dependency management for Livestore packages if you don't want to manually install all the peer dependencies yourself. -
Improved documentation (still a lot of work to do here)
-
Shows a browser dialog when trying to close a tab/window with unsaved changes
-
The SQLite leader database now uses the WAL mode to improve performance and reliability. (Thanks @IGassmann for the contribution #259.)
-
Improve Otel tracing integration
-
Fix: The query builder now correctly handles
INandNOT INwhere operations -
Fix: LiveStore crashes when using reserved keywords as a column name (
from) #245
- Changed devtools path from
/_devtools.htmlto/_livestore - General connection stability improvements
- Improved sync view:
- See sync heads in real-time
- Connect/disconnect button
- Improved eventlog view:
- Client-only mutations are now highlighted
- Added
clientId/sessionIdcolumns
- Grouped slow queries and live queries under new queries tab
- Added SQLite query playground
- Fix: Data browser now more clearly highlights selected table #239
- Reworked the Linearlite React example. (Thanks @lukaswiesehan for the contribution #248.)
- Adjusted mutation names to use past-tense
- Added Otel to
todomvcandtodomvc-sync-cfexample
- Embraced git-style push/pull semantics to sync mutations across the system
- Added node syncing integration tests
- Got rid of the coordinator abstraction in favour of a clear separation between leader and client sessions
- Renamed from
EventId.localtoEventSequenceNumber.client - Added
@livestore/sqlite-wasmpackage which wraps@livestore/wa-sqliteand exposes web and Node.js compatible VFS implementations - New devtools protocol via webmesh
- Should improve reliability of devtools connection (particularly during app reloads)
- Large refactoring to share more code between adapters
- Renamed
SynchronousDatabasetoSqliteDb - Upgrade to TypeScript 5.8
- Upgraded dependencies
- Now supports React 19
effect(needs to be 3.15.2 or higher)@livestore/wa-sqlite(needs to be 1.0.5)
- Release
- Write blog post
- Prepare X/Bluesky thread
- Refactor: Get rid of
sql-queriesmodule - API improvement: Get rid of
queryDbby exposing live queries directly on the query builder / state primitives - Optimization: Bring back rehydrating via in-memory database (requires both app and mutation db to be in-memory)
- Web adapter:
- Bug:
NotReadableError: The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired. - Refactor
shared-worker- Make it optional (for Android support)
- Make it store-agnostic (so it's reused across store instances)
- Remove extra broadcast channel for session info in @livestore/adapter-web
- Bug:
- Bug fix + testing: SQLite rollback error:
RuntimeError: null function or function signature mismatch, "note": "Failed calling makeChangeset.apply(needs repro info, probably requires property testing) - Syncing:
- introduce a way to know when an event is confirmed by the sync backend
- when no sync backend is configured, the leader sync state should not keep
pendingevents in memory - Testing: Improve sync testing (prop testing): introduce arbitrary latency for any kind of async step (~ chaos testing)
- More graceful handling when receiving a event that doesn't exist in the
local schema
- This can happen if a new app version with a new schema and an old client with the old schema tries to sync
- 2 solution paths:
- Render "upgrade app" screen
- Go offline until user upgrades the app
- Clients should detect and gracefully handle when a sync backend resets its
eventlog (e.g. during debugging)
- possibly introduce a eventlog id in the global sync metadata
@livestore/sync-cf:- Opening the snyc HTTP endpoint in the browser should provide a helpful message
- use http for initial pull while WS connection is established
- Adjust networking protocol to embrace a "walk" flow similar to how
ElectricSQL's protocol works. i.e. instead of doing 1 pull-req and getting
n pull-res back, we will adjust this to be 1:1 at the expense of slightly
higher round tripping overhead
- We will "downgrade" the purpose of the
remainingfield to be only used for UX purposes but not for correctness purposes. For correctness we will only stop pull-walking when we get an empty array back.
- We will "downgrade" the purpose of the
- Only use DO for write operations and pokes, use a separate way for pull-reqs
- Bring back "broadcast" pull res terminology
@livestore/sync-electric:- fix: connectivity state + offline handling
- implement sync payload
- Expo adapter: Fix memory leak in certain cases (needs repro info)
- Refactor/improve event sequence number implementation
- Current pain points/suboptimalities:
syncstate.ts: branching for global/client-only events- Get rid of
leaderMergeCounterTablein favour of client-only merge generation- Idea: Embed merge generation in the client-only event sequence number
- Adjust leader + client session sync processor accordingly
- Improve table schema for
eventlogtable (if possible)
- Current pain points/suboptimalities:
- Examples:
- setup: for todomvc, have a shared source of truth for the livestore definitions and have some scripts which copy them to the various example apps
- add some docs/comments to the mutations / schema definitions + link to mutation best practices (+ mention of AI linting)
- Docs
- Notes on deployment (when to deploy what)
- Document event model/eventlog design
- Unit of sharing/collaboration/auth
- What if I want got my initial container design wrong and I want to change
it?
- Comparables: document databases, kafka streams,
- Devtools
- Redesign with left sidebar
- Databrowser:
- custom handling for client-documents (i.e. render value subfields as columns) + allow value editing
- Fix: When resetting the database but keeping the eventlog
- on next app start, the app doesn't re-hydrate properly (somehow seems to "double hydrate")
- support app reloading in Expo (requires an equivalent of
beforeunloadto be triggered inmakeClientSession) - sync session appears for wrong storeid (needs repro info)
- sync view:
- different colors for when a node pulled/pushed
- show status indicators in each node: uptodate/syncing/error
- syncing should include the number of events still pending to push/pull
- maybe we can also figure out how to get the sync backend status?
- mutations explorer:
- show client events as tree
- always show root event s0
-
Added query builder API
const table = DbSchema.table('myTable', { id: DbSchema.text({ primaryKey: true }), name: DbSchema.text(), }) table.query.select('name') table.query.where('name', '==', 'Alice') table.query.where({ name: 'Alice' }) table.query.orderBy('name', 'desc').offset(10).limit(10) table.query.count().where('name', 'like', '%Ali%') table.get('123', { insertValues: { name: 'Bob' } })
-
Breaking: Renamed
querySQLtoqueryDband adjusted the signature to allow both the new query builder API and raw SQL queries:// before const query$ = querySQL(sql`select * from myTable where name = 'Alice'`, { schema: Schema.Array(table.schema), }) // after (raw SQL) const query$ = queryDb({ query: sql`select * from myTable where name = 'Alice'`, schema: Schema.Array(table.schema), }) // or with the query builder API const query$ = queryDb(table.query.select('name').where({ name: 'Alice' }))
-
Breaking: Replaced
rowQuery()withtable.get()(as part of the new query builder API)
- Fix:
useClientDocumentnow type-safe for non-nullable/non-default columns. Renamedoptions.defaultValuestooptions.insertValues
- Removed Drizzle example in favour of new query builder API
- Removed
livestore/examplesrepository in favour of/examples/standalone(additionally/examples/srcfor maintainers)
-
Breaking: Updated storage format version to 2 (will create new database files)
-
Breaking: Changed
schema.keytostoreId#175// before const schema = makeSchema({ tables, mutations, key: 'my-app-id' }) // ... <LiveStoreProvider schema={schema} storeId="my-app-id"> // after const schema = makeSchema({ tables, mutations }) // ... <LiveStoreProvider schema={schema} storeId="my-app-id">
-
Breaking: Removed
useLocalId/getLocalIdin favour ofstore.sessionId/SessionIdSymbol -
Upgraded dependencies
- If you're using
effectin your project, make sure to install version3.10.x- Note the new version of
effectnow includesSchemadirectly, so@effect/schemais no longer needed as a separate dependency. (See Effect blog post.)
- Note the new version of
- If you're using
-
Breaking: Moved
effect-db-schemato@livestore/db-schema(mostly an internal change unless you're using the package directly) -
Breaking: Adjusted
bootsignature when creating a store to now pass in aStoreinstead of a helper database object<LiveStoreProvider schema={schema} boot={(store) => store.mutate(mutations.todoCreated({ id: nanoid(), text: 'Make coffee' }))} adapter={adapter} batchUpdates={batchUpdates} > // ... </LiveStoreProvider>
-
Prepared the foundations for the upcoming rebase sync protocol
- Implementation detail: New event id strategy (uses a global event id integer sequence number and each event also keeps a reference to its parent event id)
-
Breaking: The React integration has been moved into a new separate package:
@livestore/react(before:@livestore/livestore/react) -
Breaking: Renamed
useTemporaryQuerytouseScopedQuery
-
Devtools address is now automatically logged during development making connecting easier.
-
Breaking: Changed syncing adapter interface:
const adapter = makePersistedAdapter({ storage: { type: 'opfs' }, worker: LiveStoreWorker, sharedWorker: LiveStoreSharedWorker, syncBackend: { type: 'cf', url: import.meta.env.VITE_LIVESTORE_SYNC_URL, roomId: `todomvc_${appId}`, }, })
-
Updated to Expo SDK 52 (
52.0.0-preview.23) -
Fix: Crash in release builds #206
-
Fix: Disable devtools in release builds #205
-
Fix: Databrowser no longer crashes when removing tables #189
-
Breaking (in combination with web adapter): Removed
_devtools.htmlin favour of@livestore/devtools-vite. #192-
Replace
@livestore/devtools-reactwith@livestore/devtools-vitein yourpackage.json -
Delete
_devtools.htmlif it exists -
Add the following to your
vite.config.ts:import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite' export default defineConfig({ // ... plugins: [ // ... livestoreDevtoolsPlugin({ schemaPath: './src/db/schema/index.ts' }), // ... ], })
-

