Changelog
Changelog
NOTE: LiveStore is still in alpha 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.
0.3.0
New features
-
New sync implementation (based on git-like push/pull semantics)
- See Syncing docs page for more details
sync-cf
backend: 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-wasm
build 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-electric
backend (experimental)- See docs page for more details
-
New:
@livestore/adapter-expo
now supports syncing (requires Expo 53 or later):const adapter = makePersistedAdapter({sync: { backend: makeCfSync({ 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 changes
-
Breaking: Renamed adapter packages:
@livestore/web
now is@livestore/adapter-web
@livestore/expo
now is@livestore/adapter-expo
-
Breaking: Removed
@livestore/db-schema
package and moved to@livestore/common/schema
-
Breaking: Renamed
store.mutate
tostore.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 { Schema, defineMutation } from '@livestore/livestore'// Mutations are now split up into event definitions and materializer functionsexport const todoCreated = defineMutation('todoCreated',Schema.Struct({id: DbSchema.text(),text: DbSchema.text(),}),sql`INSERT INTO todos (id, text) VALUES (${id}, ${text})`,)// schema.tsimport { 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.tsimport { State, Schema, makeSchema } 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 stateconst 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 eventsconst schema = makeSchema({ state, events }) -
Breaking
@livestore/react
: RemoveduseScopedQuery
in favour ofuseQuery
. Migration example:// beforeconst query$ = useScopedQuery(() => queryDb(tables.issues.query.where({ id: issueId }).first()), ['issue', issueId])// afterconst query$ = useQuery(queryDb(tables.issues.query.where({ id: issueId }).first(), { deps: `issue-${issueId}` })) -
Breaking
@livestore/adapter-web
: RenamedmakeAdapter
tomakePersistedAdapter
-
Breaking
@livestore/adapter-expo
: RenamedmakeAdapter
tomakePersistedAdapter
-
Breaking: Renamed
localOnly
toclientOnly
in table/mutation definitions. -
Breaking: Renamed
makeBackend
tobackend
in sync options. -
Breaking
@livestore/react
:useClientDocument
now 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.__execute
fromStore
. -
Breaking: Removed
globalReactivityGraph
and explicit passing ofreactivityGraph
to queries. -
Breaking: Removed
persisted
option 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
liveStoreStorageFormatVersion
has been bumped to3
which will create new database files. -
Breaking: Moved
queryGraphQL
to@livestore/graphql
and thus removinggraphql
from peer dependencies of@livestore/livestore
. -
Moved dev helper methods from e.g.
store.__devDownloadDb()
tostore._dev.downloadDb()
-
Breaking
@livestore/sync-cf
: RenamedmakeWsSync
tomakeCfSync
Notable improvements & fixes
-
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-deps
package 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
IN
andNOT IN
where operations -
Fix: LiveStore crashes when using reserved keywords as a column name (
from
) #245
Devtools
- Changed devtools path from
/_devtools.html
to/_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
/sessionId
columns
- Grouped slow queries and live queries under new queries tab
- Added SQLite query playground
- Fix: Data browser now more clearly highlights selected table #239
Examples
- Reworked the Linearlite React example. (Thanks @lukaswiesehan for the contribution #248.)
- Adjusted mutation names to use past-tense
- Added Otel to
todomvc
andtodomvc-sync-cf
example
Internal changes
- 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 field from
EventId.local
toEventId.client
- Added
@livestore/sqlite-wasm
package which wraps@livestore/wa-sqlite
and 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
SynchronousDatabase
toSqliteDb
- Upgrade to TypeScript 5.8
- Upgraded dependencies
- Now supports React 19
effect
(needs to be 3.14.8 or higher)@livestore/wa-sqlite
(needs to be 1.0.5-dev.2)
Still todo:
- Syncing
- when no sync backend is configured, the leader sync state should not keep
pending
events in memory - Refactor: Rename
EventId
toEventNumber
- Attempts sync push after read-model re-creation leading to some other bugs: (see https://share.cleanshot.com/hQ269Fkc)
- Get rid of
migrationOptions
as part of this fix (also document in changelog once done)
- Get rid of
- 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
- introduce a way to know when a mutation is confirmed by the sync backend
- cf sync:
- 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
remaining
field 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
- Bring back “broadcast” pull res terminology
- Electric:
- fix: connectivity state + offline handling
- implement sync payload
- Clients should detect and gracefully handle when a sync backend resets its eventlog (e.g. during debugging)
- when no sync backend is configured, the leader sync state should not keep
- Devtools
- 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”)
- Devtools lose connection to client session (https://share.cleanshot.com/dK2rvyyj)
- Expo devtools: use node adapter ws server
- sync session appears for wrong storeid
- 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
- Fix: When resetting the database but keeping the eventlog
- Release
- Write blog post
- Prepare X/Bluesky thread
After release:
- Get rid of
sql-queries
module - Get rid of
queryDb
by exposing live queries directly on the query builder / state primitives - Bring back rehydrating via in-memory database (requires both app and mutation db to be in-memory)
- chrome extension: Prevent service worker from going inactive (otherwise extension worker message channels will also go down)
- Improve sync testing (prop testing): introduce arbitrary latency for any kind of async step (~ chaos testing)
- 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)
- Embrace term “containers”
- 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,
0.2.0
Core
-
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
querySQL
toqueryDb
and adjusted the signature to allow both the new query builder API and raw SQL queries:// beforeconst 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 APIconst query$ = queryDb(table.query.select('name').where({ name: 'Alice' })) -
Breaking: Replaced
rowQuery()
withtable.get()
(as part of the new query builder API)
React integration
- Fix:
useClientDocument
now type-safe for non-nullable/non-default columns. Renamedoptions.defaultValues
tooptions.insertValues
Misc
- Removed Drizzle example in favour of new query builder API
- Removed
livestore/examples
repository in favour of/examples/standalone
(additionally/examples/src
for maintainers)
0.1.0
Core
-
Breaking: Updated storage format version to 2 (will create new database files)
-
Breaking: Changed
schema.key
tostoreId
#175// beforeconst schema = makeSchema({ tables, mutations, key: 'my-app-id' })// ...<LiveStoreProvider schema={schema} storeId="my-app-id">// afterconst schema = makeSchema({ tables, mutations })// ...<LiveStoreProvider schema={schema} storeId="my-app-id"> -
Breaking: Removed
useLocalId
/getLocalId
in favour ofstore.sessionId
/SessionIdSymbol
-
Upgraded dependencies
- If you’re using
effect
in your project, make sure to install version3.10.x
- Note the new version of
effect
now includesSchema
directly, so@effect/schema
is no longer needed as a separate dependency. (See Effect blog post.)
- Note the new version of
- If you’re using
-
Breaking: Moved
effect-db-schema
to@livestore/db-schema
(mostly an internal change unless you’re using the package directly) -
Breaking: Adjusted
boot
signature when creating a store to now pass in aStore
instead of a helper database object<LiveStoreProviderschema={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)
React integration
-
Breaking: The React integration has been moved into a new separate package:
@livestore/react
(before:@livestore/livestore/react
) -
Breaking: Renamed
useTemporaryQuery
touseScopedQuery
Web adapter
-
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}`,},})
Expo adapter
-
Updated to Expo SDK 52 (
52.0.0-preview.23
) -
Fix: Crash in release builds #206
-
Fix: Disable devtools in release builds #205
Devtools
-
Feature: New SQLite query playground
-
Fix: Databrowser no longer crashes when removing tables #189
-
Breaking (in combination with web adapter): Removed
_devtools.html
in favour of@livestore/devtools-vite
. #192-
Replace
@livestore/devtools-react
with@livestore/devtools-vite
in yourpackage.json
-
Delete
_devtools.html
if 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' }),// ...],})
-