Skip to content

Store

The Store is the most common way to interact with LiveStore from your application code. It provides a way to query data, commit events, and subscribe to data changes.

For how to create a store in React, see the React integration docs. The following example shows how to create a store manually:

import {
const makeAdapter: ({ sync, ...options }: NodeAdapterOptions & {
sync?: SyncOptions;
}) => Adapter

Creates a single-threaded LiveStore adapter for Node.js applications.

This adapter runs the leader thread (persistence and sync) in the same thread as your application. Suitable for CLI tools, scripts, and applications where simplicity is preferred over maximum performance.

For production servers or performance-critical applications, consider makeWorkerAdapter which runs persistence/sync in a separate worker thread.

@example

import { makeAdapter } from '@livestore/adapter-node'
import { makeWsSync } from '@livestore/sync-cf/client'
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
sync: {
backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
},
})

@example

// With DevTools support
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
devtools: {
schemaPath: new URL('./schema.ts', import.meta.url),
port: 4242,
},
})

@seehttps://livestore.dev/docs/reference/adapters/node for setup guide

makeAdapter
} from '@livestore/adapter-node'
import {
const createStorePromise: <TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}, TSyncPayloadSchema extends Codec<Json, Json> = Codec<Json, Json, never, never>>({ signal, otelOptions, ...options }: CreateStoreOptionsPromise<TSchema, TContext, TSyncPayloadSchema>) => Promise<Store<TSchema, TContext>>

Create a new LiveStore Store

createStorePromise
} from '@livestore/livestore'
import {
import schema
schema
} from './schema.ts'
const
const adapter: Adapter
adapter
=
function makeAdapter({ sync, ...options }: NodeAdapterOptions & {
sync?: SyncOptions;
}): Adapter

Creates a single-threaded LiveStore adapter for Node.js applications.

This adapter runs the leader thread (persistence and sync) in the same thread as your application. Suitable for CLI tools, scripts, and applications where simplicity is preferred over maximum performance.

For production servers or performance-critical applications, consider makeWorkerAdapter which runs persistence/sync in a separate worker thread.

@example

import { makeAdapter } from '@livestore/adapter-node'
import { makeWsSync } from '@livestore/sync-cf/client'
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
sync: {
backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
},
})

@example

// With DevTools support
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
devtools: {
schemaPath: new URL('./schema.ts', import.meta.url),
port: 4242,
},
})

@seehttps://livestore.dev/docs/reference/adapters/node for setup guide

makeAdapter
({
NodeAdapterOptions.storage: {
readonly type: ["in-memory"];
readonly importSnapshot?: any;
} | {
readonly type: ["fs"];
readonly baseDirectory?: string | undefined;
}
storage
: {
type: string
type
: 'fs' },
// sync: { backend: makeWsSync({ url: '...' }) },
})
export const
const bootstrap: () => Promise<Store<any, {}>>
bootstrap
= async () => {
const
const store: Store<any, {}>
store
= await
createStorePromise<any, {}, Codec<Json, Json, never, never>>({ signal, otelOptions, ...options }: CreateStoreOptionsPromise<any, {}, Codec<Json, Json, never, never>>): Promise<Store<any, {}>>

Create a new LiveStore Store

createStorePromise
({
CreateStoreOptions<any, {}, Codec<Json, Json, never, never>>.schema: any

The LiveStore schema defining tables, events, and materializers.

schema
,
CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}, TSyncPayloadSchema extends Codec<Json, Json> = Codec<Json, Json, never, never>>.adapter: Adapter

Adapter used for data storage and synchronization.

adapter
,
CreateStoreOptions<TSchema extends LiveStoreSchema, TContext = {}, TSyncPayloadSchema extends Codec<Json, Json> = Codec<Json, Json, never, never>>.storeId: string

Unique identifier for the Store instance, stable for its lifetime.

  • Valid characters: Only alphanumeric characters, underscores (_), and hyphens (-) are allowed. Must match /^[a-zA-Z0-9_-]+$/.
  • Globally unique: Use globally unique IDs (e.g., nanoid) to prevent collisions across stores.
  • Use namespaces: Prefix to avoid collisions and for easier identification when debugging (e.g., app-root, workspace-abc123, issue-456)

storeId
: 'some-store-id',
})
return
const store: Store<any, {}>
store
}
import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import {
import storeTables
storeTables
} from './schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
const
const todos: unknown
todos
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.query: <unknown>(query: Queryable<unknown> | {
query: string;
bindValues: Bindable;
schema?: Decoder<unknown, never>;
}, options?: {
otelContext?: Context;
debugRefreshReason?: RefreshReason;
}) => unknown

Synchronously queries the database without creating a LiveQuery. This is useful for queries that don't need to be reactive.

Example: Query builder

const completedTodos = store.query(tables.todo.where({ complete: true }))

Example: Raw SQL query

const completedTodos = store.query({ query: 'SELECT * FROM todo WHERE complete = 1', bindValues: {} })

query
(
import storeTables
storeTables
.
any
todos
)
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const todos: unknown
todos
)
import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import {
import storeTables
storeTables
} from './schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
const
const unsubscribe: Unsubscribe
unsubscribe
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.subscribe: <unknown>(query: Queryable<unknown>, onUpdate: (value: unknown) => void, options?: SubscribeOptions<unknown> | undefined) => Unsubscribe (+1 overload)
subscribe
(
import storeTables
storeTables
.
any
todos
, (
todos: unknown
todos
) => {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
(
todos: unknown
todos
)
})
const unsubscribe: () => void
unsubscribe
()
import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
import {
import storeEvents
storeEvents
} from './schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [any]>(list_0: any) => void (+3 overloads)
commit
(
import storeEvents
storeEvents
.
any
todoCreated
({
id: string
id
: '1',
text: string
text
: 'Buy milk' }))

Currently only events confirmed by the sync backend are supported.

import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
// Run once
for await (const
const event: Decoded<any>
event
of
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.events: (options?: StoreEventsOptions<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any> | undefined) => AsyncIterable<Decoded<any>>

Returns an async iterable of events from the eventlog. Currently only events confirmed by the sync backend is supported.

Defaults to tracking upstreamHead as it advances. If an until event is supplied the stream finalizes upon reaching it.

To start streaming from a specific point in the eventlog you can provide a since event.

Allows filtering by:

  • filter: event types
  • clientIds: client identifiers
  • sessionIds: session identifiers

The batchSize option controls the maximum amount of events that are fetched from the eventlog in each query. Defaults to 100 and has a max allowed value of 1000.

TODO:

  • Support streaming unconfirmed events
  • Leader level
  • Session level
  • Support streaming client-only events

@example

// Stream todoCompleted events from the start
for await (const event of store.events(filter: ['todoCompleted'])) {
console.log(event)
}

@example

// Start streaming from a specific event
for await (const event of store.events({ since: EventSequenceNumber.Client.fromString('e3') })) {
console.log(event)
}

events
()) {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
('event from leader',
const event: Decoded<any>
event
)
}
// Continuos stream
const
const iterator: AsyncIterator<Decoded<any>, any, any>
iterator
=
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.events: (options?: StoreEventsOptions<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any> | undefined) => AsyncIterable<Decoded<any>>

Returns an async iterable of events from the eventlog. Currently only events confirmed by the sync backend is supported.

Defaults to tracking upstreamHead as it advances. If an until event is supplied the stream finalizes upon reaching it.

To start streaming from a specific point in the eventlog you can provide a since event.

Allows filtering by:

  • filter: event types
  • clientIds: client identifiers
  • sessionIds: session identifiers

The batchSize option controls the maximum amount of events that are fetched from the eventlog in each query. Defaults to 100 and has a max allowed value of 1000.

TODO:

  • Support streaming unconfirmed events
  • Leader level
  • Session level
  • Support streaming client-only events

@example

// Stream todoCompleted events from the start
for await (const event of store.events(filter: ['todoCompleted'])) {
console.log(event)
}

@example

// Start streaming from a specific event
for await (const event of store.events({ since: EventSequenceNumber.Client.fromString('e3') })) {
console.log(event)
}

events
()[
var Symbol: SymbolConstructor
Symbol
.
SymbolConstructor.asyncIterator: typeof Symbol.asyncIterator

A method that returns the default async iterator for an object. Called by the semantics of the for-await-of statement.

asyncIterator
]()
try {
while (true) {
const {
const value: any
value
,
const done: boolean | undefined
done
} = await
const iterator: AsyncIterator<Decoded<any>, any, any>
iterator
.
AsyncIterator<Decoded<any>, any, any>.next(...[value]: [] | [any]): Promise<IteratorResult<Decoded<any>, any>>
next
()
if (
const done: boolean | undefined
done
=== true) break
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

The console.log() static method outputs a message to the console.

MDN Reference

log
('event from stream:',
const value: Decoded<any>
value
)
}
} finally {
await
const iterator: AsyncIterator<Decoded<any>, any, any>
iterator
.
AsyncIterator<Decoded<any>, any, any>.return?(value?: any): Promise<IteratorResult<Decoded<any>, any>>
return
?.()
}

LiveStore provides two APIs for shutting down a store:

import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
} from '@livestore/livestore'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>

Central interface to a LiveStore database providing reactive queries, event commits, and sync.

A Store instance wraps a local SQLite database that is kept in sync with other clients via an event log. Instead of mutating state directly, you commit events that get materialized into database rows. Queries automatically re-run when their underlying tables change.

Creating a Store

Use createStore (Effect-based) or createStorePromise to obtain a Store instance. In React applications, use StoreRegistry with <StoreRegistryProvider> and the useStore() hook which manages the Store lifecycle.

Querying Data

Use

Store.query

for one-shot reads or

Store.subscribe

for reactive subscriptions. Both accept query builders (e.g. tables.todo.where({ complete: true })) or custom LiveQueryDefs.

Committing Events

Use

Store.commit

to persist events. Events are immediately materialized locally and asynchronously synced to other clients. Multiple events can be committed atomically.

Lifecycle

The Store must be shut down when no longer needed via

Store.shutdown

or

Store.shutdownPromise

. Framework integrations (React, Effect) handle this automatically.

@example

// Query data
const todos = store.query(tables.todo.where({ complete: false }))
// Subscribe to changes
const unsubscribe = store.subscribe(tables.todo.all(), (todos) => {
console.log('Todos updated:', todos)
})
// Commit an event
store.commit(events.todoCreated({ id: nanoid(), text: 'Buy milk' }))

Store
const
const effectShutdown: Effect.Effect<void, never, never>
effectShutdown
=
import Effect
Effect
.
const gen: <Effect.Effect<void, never, never>, void>(f: () => Generator<Effect.Effect<void, never, never>, void, never>) => Effect.Effect<void, never, never> (+1 overload)

Provides a way to write effectful code using generator functions, simplifying control flow and error handling.

When to use

Use when you want to write effectful code that looks and behaves like synchronous code, while still handling asynchronous tasks, errors, and complex control flow such as loops and conditions.

Generator functions work similarly to async/await but keep errors, requirements, and interruption in the Effect type. You can yield* values from effects and return the final result at the end.

Example (Sequencing effects with generators)

import { Data, Effect } from "effect"
class DiscountRateError extends Data.TaggedError("DiscountRateError")<{}> {}
const addServiceCharge = (amount: number) => amount + 1
const applyDiscount = (
total: number,
discountRate: number
): Effect.Effect<number, DiscountRateError> =>
discountRate === 0
? Effect.fail(new DiscountRateError())
: Effect.succeed(total - (total * discountRate) / 100)
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const fetchDiscountRate = Effect.promise(() => Promise.resolve(5))
export const program = Effect.gen(function*() {
const transactionAmount = yield* fetchTransactionAmount
const discountRate = yield* fetchDiscountRate
const discountedAmount = yield* applyDiscount(
transactionAmount,
discountRate
)
const finalAmount = addServiceCharge(discountedAmount)
return `Final amount to charge: ${finalAmount}`
})

@since2.0.0

gen
(function* () {
yield*
import Effect
Effect
.
const log: (...message: ReadonlyArray<any>) => Effect.Effect<void>

Logs one or more messages using the default log level.

Example (Logging at the default level)

import { Effect } from "effect"
const program = Effect.gen(function*() {
yield* Effect.log("Starting computation")
const result = 2 + 2
yield* Effect.log("Result:", result)
yield* Effect.log("Multiple", "values", "can", "be", "logged")
return result
})
Effect.runPromise(program).then(console.log)
// Output:
// timestamp=2023-... level=INFO message="Starting computation"
// timestamp=2023-... level=INFO message="Result: 4"
// timestamp=2023-... level=INFO message="Multiple values can be logged"
// 4

@since2.0.0

log
('Shutting down store')
yield*
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.shutdown: (cause?: Cause<UnknownError | MaterializeError>) => Effect.Effect<void>

Shuts down the store and closes the client session.

This is called automatically when the store was created using the React or Effect API.

shutdown
()
})
const
const shutdownWithPromise: () => Promise<void>
shutdownWithPromise
= async () => {
await
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.shutdownPromise: (cause?: UnknownError) => Promise<void>

Shuts down the store and closes the client session.

This is called automatically when the store was created using the React or Effect API.

shutdownPromise
()
}

For applications using Effect, LiveStore provides a type-safe way to access stores through the Effect layer system via makeStoreContext().

Use makeStoreContext() to create a typed context that preserves your schema types:

// Define a typed store context with your schema
export const
const TodoStore: StoreTagClass<any, "todos">
TodoStore
=
const Store: {
Tag: <TSchema extends LiveStoreSchema, TStoreId extends string>(schema: TSchema, storeId: TStoreId) => StoreTagClass<TSchema, TStoreId>;
}

Store utilities for Effect integration.

@example

import { Store } from '@livestore/livestore/effect'
export class MainStore extends Store.Tag(schema, 'main') {}

Store
.
type Tag: <any, "todos">(schema: any, storeId: "todos") => StoreTagClass<any, "todos">

Create a typed store context class for use with Effect.

Returns a class that extends Context.Service, making it directly yieldable in Effect code. The class includes static methods for creating layers and accessors for common operations.

@paramschema - The LiveStore schema (used for type inference and runtime)

@paramstoreId - Unique identifier for this store

@example

Basic usage

import { Store } from '@livestore/livestore/effect'
import { schema } from './schema.ts'
// Define your store (once per store)
export class MainStore extends Store.Tag(schema, 'main') {}
// Create the layer
const storeLayer = MainStore.layer({
adapter: myAdapter,
batchUpdates: ReactDOM.unstable_batchedUpdates,
})
// Use in Effect code
Effect.gen(function* () {
const { store } = yield* MainStore
// ^? Store<typeof schema> - fully typed!
// Or use accessors
const users = yield* MainStore.query(tables.users.all())
yield* MainStore.commit(events.createUser({ id: '1', name: 'Alice' }))
})

@example

Multiple stores

class MainStore extends Store.Tag(mainSchema, 'main') {}
class SettingsStore extends Store.Tag(settingsSchema, 'settings') {}
// Both available in same Effect context
Effect.gen(function* () {
const main = yield* MainStore
const settings = yield* SettingsStore
})
const layer = Layer.mergeAll(
MainStore.layer({ adapter: mainAdapter }),
SettingsStore.layer({ adapter: settingsAdapter }),
)

Tag
(
import schema
schema
, 'todos')
// Create a layer to initialize the store
const
const adapter: Adapter
adapter
=
function makeAdapter({ sync, ...options }: NodeAdapterOptions & {
sync?: SyncOptions;
}): Adapter

Creates a single-threaded LiveStore adapter for Node.js applications.

This adapter runs the leader thread (persistence and sync) in the same thread as your application. Suitable for CLI tools, scripts, and applications where simplicity is preferred over maximum performance.

For production servers or performance-critical applications, consider makeWorkerAdapter which runs persistence/sync in a separate worker thread.

@example

import { makeAdapter } from '@livestore/adapter-node'
import { makeWsSync } from '@livestore/sync-cf/client'
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
sync: {
backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
},
})

@example

// With DevTools support
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
devtools: {
schemaPath: new URL('./schema.ts', import.meta.url),
port: 4242,
},
})

@seehttps://livestore.dev/docs/reference/adapters/node for setup guide

makeAdapter
({
NodeAdapterOptions.storage: {
readonly type: ["in-memory"];
readonly importSnapshot?: any;
} | {
readonly type: ["fs"];
readonly baseDirectory?: string | undefined;
}
storage
: {
type: string
type
: 'fs' } })
adapter: Adapter
adapter
,
batchUpdates: (run: () => void) => void
batchUpdates
: (
cb: () => void
cb
) =>
cb: () => void
cb
(), // For Node.js; use React's unstable_batchedUpdates in React apps
})
import {
const defineMaterializer: <TEventDef extends State.SQLite.EventDef.AnyWithoutFn>(_eventDef: TEventDef, materializer: State.SQLite.Materializer<TEventDef>) => State.SQLite.Materializer<TEventDef>

Type-safe wrapper for defining a single materializer.

Useful when defining materializers separately from the materializers() builder. The first argument provides type inference for the second.

@example

const todoCreatedHandler = defineMaterializer(
events.todoCreated,
({ id, text }) => tables.todos.insert({ id, text, completed: false })
)

defineMaterializer
,
import Events
Events
,
const makeSchema: <TInputSchema extends InputSchema>(inputSchema: TInputSchema) => FromInputSchema.DeriveSchema<TInputSchema>
makeSchema
,
import Schema
Schema
,
import State
State
} from '@livestore/livestore'
const
const tables: {
readonly todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>;
}
tables
= {

The factory takes your schema type as a generic parameter and returns a StoreContext with:

  • Tag - Context tag for dependency injection
  • Layer - Creates a layer that initializes the store
  • DeferredTag - For async initialization patterns
  • DeferredLayer - Layer providing the deferred context
  • fromDeferred - Layer that waits for deferred initialization

Access the store in Effect code with full type safety and autocomplete:

// Define a typed store context with your schema
export const
const TodoStore: StoreTagClass<FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Encoded">>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", Struct.ReadonlySide<{
readonly id: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
}, "Encoded">>;
};
state: InternalState;
}>, "todos">
TodoStore
=
const Store: {
Tag: <TSchema extends LiveStoreSchema, TStoreId extends string>(schema: TSchema, storeId: TStoreId) => StoreTagClass<TSchema, TStoreId>;
}

Store utilities for Effect integration.

@example

import { Store } from '@livestore/livestore/effect'
export class MainStore extends Store.Tag(schema, 'main') {}

Store
.
type Tag: <FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", Struct<Fields extends Struct.Fields>.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Encoded">>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", Struct.ReadonlySide<{
readonly id: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
}, "Encoded">>;
};
state: InternalState;
}>, "todos">(schema: FromInputSchema.DeriveSchema<...>, storeId: "todos") => StoreTagClass<...>

Create a typed store context class for use with Effect.

Returns a class that extends Context.Service, making it directly yieldable in Effect code. The class includes static methods for creating layers and accessors for common operations.

@paramschema - The LiveStore schema (used for type inference and runtime)

@paramstoreId - Unique identifier for this store

@example

Basic usage

import { Store } from '@livestore/livestore/effect'
import { schema } from './schema.ts'
// Define your store (once per store)
export class MainStore extends Store.Tag(schema, 'main') {}
// Create the layer
const storeLayer = MainStore.layer({
adapter: myAdapter,
batchUpdates: ReactDOM.unstable_batchedUpdates,
})
// Use in Effect code
Effect.gen(function* () {
const { store } = yield* MainStore
// ^? Store<typeof schema> - fully typed!
// Or use accessors
const users = yield* MainStore.query(tables.users.all())
yield* MainStore.commit(events.createUser({ id: '1', name: 'Alice' }))
})

@example

Multiple stores

class MainStore extends Store.Tag(mainSchema, 'main') {}
class SettingsStore extends Store.Tag(settingsSchema, 'settings') {}
// Both available in same Effect context
Effect.gen(function* () {
const main = yield* MainStore
const settings = yield* SettingsStore
})
const layer = Layer.mergeAll(
MainStore.layer({ adapter: mainAdapter }),
SettingsStore.layer({ adapter: settingsAdapter }),
)

Tag
(
const schema: FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Encoded">>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", Struct.ReadonlySide<{
readonly id: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
}, "Encoded">>;
};
state: InternalState;
}>
schema
, 'todos')
// Create a layer to initialize the store
const
const adapter: Adapter
adapter
=
function makeAdapter({ sync, ...options }: NodeAdapterOptions & {
sync?: SyncOptions;
}): Adapter

Creates a single-threaded LiveStore adapter for Node.js applications.

This adapter runs the leader thread (persistence and sync) in the same thread as your application. Suitable for CLI tools, scripts, and applications where simplicity is preferred over maximum performance.

For production servers or performance-critical applications, consider makeWorkerAdapter which runs persistence/sync in a separate worker thread.

@example

import { makeAdapter } from '@livestore/adapter-node'
import { makeWsSync } from '@livestore/sync-cf/client'
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
sync: {
backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
},
})

@example

// With DevTools support
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
devtools: {
schemaPath: new URL('./schema.ts', import.meta.url),
port: 4242,
},
})

@seehttps://livestore.dev/docs/reference/adapters/node for setup guide

makeAdapter
({
NodeAdapterOptions.storage: {
readonly type: ["in-memory"];
readonly importSnapshot?: any;
} | {
readonly type: ["fs"];
readonly baseDirectory?: string | undefined;
}
storage
: {
type: string
type
: 'fs' } })
adapter: Adapter
adapter
,
batchUpdates: (run: () => void) => void
batchUpdates
: (
cb: () => void
cb
) =>
cb: () => void
cb
(), // For Node.js; use React's unstable_batchedUpdates in React apps
})
import {
const defineMaterializer: <TEventDef extends State.SQLite.EventDef.AnyWithoutFn>(_eventDef: TEventDef, materializer: State.SQLite.Materializer<TEventDef>) => State.SQLite.Materializer<TEventDef>

Type-safe wrapper for defining a single materializer.

Useful when defining materializers separately from the materializers() builder. The first argument provides type inference for the second.

@example

const todoCreatedHandler = defineMaterializer(
events.todoCreated,
({ id, text }) => tables.todos.insert({ id, text, completed: false })
)

defineMaterializer
,
import Events
Events
,
const makeSchema: <TInputSchema extends InputSchema>(inputSchema: TInputSchema) => FromInputSchema.DeriveSchema<TInputSchema>
makeSchema
,
import Schema
Schema
,
import State
State
} from '@livestore/livestore'
const
const tables: {
readonly todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>;
}
tables
= {
todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>
todos
:
import State
State
.
import SQLite
SQLite
.
function table<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}, Partial<...>>(args: {
...;
} & Partial<...>): State.SQLite.TableDef<...> (+2 overloads)

Creates a SQLite table definition from columns or an Effect Schema.

This function supports two main ways to define a table:

  1. Using explicit column definitions
  2. Using an Effect Schema (either the name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columns
const usersTable = State.SQLite.table({
name: 'users',
columns: {
id: State.SQLite.text({ primaryKey: true }),
name: State.SQLite.text({ nullable: false }),
email: State.SQLite.text({ nullable: false }),
age: State.SQLite.integer({ nullable: true }),
},
})
// Using Effect Schema with annotations
import { Schema } from '@livestore/utils/effect'
const UserSchema = Schema.Struct({
id: Schema.Int.pipe(State.SQLite.withPrimaryKey).pipe(State.SQLite.withAutoIncrement),
email: Schema.String.pipe(State.SQLite.withUnique),
name: Schema.String,
active: Schema.Boolean.pipe(State.SQLite.withDefault(true)),
createdAt: Schema.optional(Schema.Date),
})
// Option 1: With explicit name
const usersTable = State.SQLite.table({
name: 'users',
schema: UserSchema,
})
// Option 2: With name from schema annotation (title or identifier)
const AnnotatedUserSchema = UserSchema.annotate({ title: 'users' })
const usersTable2 = State.SQLite.table({
schema: AnnotatedUserSchema,
})
// Adding indexes
const PostSchema = Schema.Struct({
id: Schema.String.pipe(State.SQLite.withPrimaryKey),
title: Schema.String,
authorId: Schema.String,
createdAt: Schema.Date,
}).annotate({ identifier: 'posts' })
const postsTable = State.SQLite.table({
schema: PostSchema,
indexes: [
{ name: 'idx_posts_author', columns: ['authorId'] },
{ name: 'idx_posts_created', columns: ['createdAt'], isUnique: false },
],
})

table
({
name: "todos"
name
: 'todos',
columns: {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}
columns
: {
id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
}
id
:
import State
State
.
import SQLite
SQLite
.
const text: <string, string, false, typeof NoDefault, true, false>(args: {
schema?: Schema.Codec<string, string, never, never>;
default?: typeof NoDefault;
nullable?: false;
primaryKey?: true;
autoIncrement?: false;
}) => {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
} (+1 overload)
text
({
primaryKey?: true
primaryKey
: true }),
text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
}
text
:
import State
State
.
import SQLite
SQLite
.
const text: () => {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
} (+1 overload)
text
(),
completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
}
completed
:
import State
State
.
import SQLite
SQLite
.
const boolean: <boolean, false, false, false, false>(args: {
default?: false;
nullable?: false;
primaryKey?: false;
autoIncrement?: false;
}) => {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
} (+1 overload)
boolean
({
default?: false
default
: false }),
},
}),
} as
type const = {
readonly todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>;
}
const
const
const events: {
readonly todoCreated: State.SQLite.EventDef<"v1.TodoCreated", Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Encoded">>;
readonly todoCompleted: State.SQLite.EventDef<"v1.TodoCompleted", Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">>;
}
events
= {
todoCreated: State.SQLite.EventDef<"v1.TodoCreated", Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Encoded">>
todoCreated
:
import Events
Events
.
synced<"v1.TodoCreated", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Encoded">>(args: {
name: "v1.TodoCreated";
schema: Schema.Codec<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Encoded">, never, never>;
} & Omit<...>): State.SQLite.EventDef<...>
export synced

Creates a synced event definition.

Synced events are sent to the sync backend and distributed to all connected clients. Use this for collaborative data that should be shared across users and devices.

Event names should be versioned (e.g., v1.TodoCreated) to support schema evolution over time.

@example

import { Events } from '@livestore/livestore'
import { Schema } from 'effect'
const todoCreated = Events.synced({
name: 'v1.TodoCreated',
schema: Schema.Struct({
id: Schema.String,
text: Schema.String,
completed: Schema.Boolean,
}),
})
// Commit the event
store.commit(todoCreated({ id: 'abc', text: 'Buy milk', completed: false }))

synced
({
name: "v1.TodoCreated"
name
: 'v1.TodoCreated',
schema: Schema.Codec<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
readonly text: Schema.String;
}, "Encoded">, never, never>
schema
:
import Schema
Schema
.
function Struct<{
readonly id: Schema.String;
readonly text: Schema.String;
}>(fields: {
readonly id: Schema.String;
readonly text: Schema.String;
}): Schema.Struct<{
readonly id: Schema.String;
readonly text: Schema.String;
}>

Defines a struct schema from a map of field schemas.

Details

Each field value is a schema. Use

optionalKey

or

optional

to mark fields as optional, and

mutableKey

to mark them as mutable.

The resulting schema's Type is a readonly object type with the fields' decoded types. The Encoded form mirrors the field schemas' encoded types.

Example (Defining a basic struct)

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optionalKey(Schema.String)
})
// { readonly name: string; readonly age: number; readonly email?: string }
type Person = typeof Person.Type
const alice = Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 })
console.log(alice)
// { name: 'Alice', age: 30 }

@since3.10.0

Struct
({
id: Schema.String
id
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@since4.0.0

@since4.0.0

String
,
text: Schema.String
text
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@since4.0.0

@since4.0.0

String
}),

Compose store layers with your application services:

// Define a typed store context with your schema
export const
const TodoStore: StoreTagClass<FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Encoded">>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", Struct.ReadonlySide<{
readonly id: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
}, "Encoded">>;
};
state: InternalState;
}>, "todos">
TodoStore
=
const Store: {
Tag: <TSchema extends LiveStoreSchema, TStoreId extends string>(schema: TSchema, storeId: TStoreId) => StoreTagClass<TSchema, TStoreId>;
}

Store utilities for Effect integration.

@example

import { Store } from '@livestore/livestore/effect'
export class MainStore extends Store.Tag(schema, 'main') {}

Store
.
type Tag: <FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", Struct<Fields extends Struct.Fields>.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Encoded">>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", Struct.ReadonlySide<{
readonly id: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
}, "Encoded">>;
};
state: InternalState;
}>, "todos">(schema: FromInputSchema.DeriveSchema<...>, storeId: "todos") => StoreTagClass<...>

Create a typed store context class for use with Effect.

Returns a class that extends Context.Service, making it directly yieldable in Effect code. The class includes static methods for creating layers and accessors for common operations.

@paramschema - The LiveStore schema (used for type inference and runtime)

@paramstoreId - Unique identifier for this store

@example

Basic usage

import { Store } from '@livestore/livestore/effect'
import { schema } from './schema.ts'
// Define your store (once per store)
export class MainStore extends Store.Tag(schema, 'main') {}
// Create the layer
const storeLayer = MainStore.layer({
adapter: myAdapter,
batchUpdates: ReactDOM.unstable_batchedUpdates,
})
// Use in Effect code
Effect.gen(function* () {
const { store } = yield* MainStore
// ^? Store<typeof schema> - fully typed!
// Or use accessors
const users = yield* MainStore.query(tables.users.all())
yield* MainStore.commit(events.createUser({ id: '1', name: 'Alice' }))
})

@example

Multiple stores

class MainStore extends Store.Tag(mainSchema, 'main') {}
class SettingsStore extends Store.Tag(settingsSchema, 'settings') {}
// Both available in same Effect context
Effect.gen(function* () {
const main = yield* MainStore
const settings = yield* SettingsStore
})
const layer = Layer.mergeAll(
MainStore.layer({ adapter: mainAdapter }),
SettingsStore.layer({ adapter: settingsAdapter }),
)

Tag
(
const schema: FromInputSchema.DeriveSchema<{
events: {
readonly todoCreated: EventDef<"v1.TodoCreated", Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
readonly text: String;
}, "Encoded">>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", Struct.ReadonlySide<{
readonly id: String;
}, "Type">, Struct.ReadonlySide<{
readonly id: String;
}, "Encoded">>;
};
state: InternalState;
}>
schema
, 'todos')
// Create a layer to initialize the store
const
const adapter: Adapter
adapter
=
function makeAdapter({ sync, ...options }: NodeAdapterOptions & {
sync?: SyncOptions;
}): Adapter

Creates a single-threaded LiveStore adapter for Node.js applications.

This adapter runs the leader thread (persistence and sync) in the same thread as your application. Suitable for CLI tools, scripts, and applications where simplicity is preferred over maximum performance.

For production servers or performance-critical applications, consider makeWorkerAdapter which runs persistence/sync in a separate worker thread.

@example

import { makeAdapter } from '@livestore/adapter-node'
import { makeWsSync } from '@livestore/sync-cf/client'
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
sync: {
backend: makeWsSync({ url: 'wss://api.example.com/sync' }),
},
})

@example

// With DevTools support
const adapter = makeAdapter({
storage: { type: 'fs', baseDirectory: './data' },
devtools: {
schemaPath: new URL('./schema.ts', import.meta.url),
port: 4242,
},
})

@seehttps://livestore.dev/docs/reference/adapters/node for setup guide

makeAdapter
({
NodeAdapterOptions.storage: {
readonly type: ["in-memory"];
readonly importSnapshot?: any;
} | {
readonly type: ["fs"];
readonly baseDirectory?: string | undefined;
}
storage
: {
type: string
type
: 'fs' } })
adapter: Adapter
adapter
,
batchUpdates: (run: () => void) => void
batchUpdates
: (
cb: () => void
cb
) =>
cb: () => void
cb
(), // For Node.js; use React's unstable_batchedUpdates in React apps
})
import {
const defineMaterializer: <TEventDef extends State.SQLite.EventDef.AnyWithoutFn>(_eventDef: TEventDef, materializer: State.SQLite.Materializer<TEventDef>) => State.SQLite.Materializer<TEventDef>

Type-safe wrapper for defining a single materializer.

Useful when defining materializers separately from the materializers() builder. The first argument provides type inference for the second.

@example

const todoCreatedHandler = defineMaterializer(
events.todoCreated,
({ id, text }) => tables.todos.insert({ id, text, completed: false })
)

defineMaterializer
,
import Events
Events
,
const makeSchema: <TInputSchema extends InputSchema>(inputSchema: TInputSchema) => FromInputSchema.DeriveSchema<TInputSchema>
makeSchema
,
import Schema
Schema
,
import State
State
} from '@livestore/livestore'
const
const tables: {
readonly todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
readonly completed: {
columnType: "integer";
schema: Schema.Codec<boolean, number, never, never>;
default: Some<false>;
nullable: false;
primaryKey: false;
autoIncrement: false;
};
}>, State.SQLite.WithDefaults<...>, Schema.Struct<...>>;
}
tables
= {
todos: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"todos", {
readonly id: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: true;
autoIncrement: false;
};
readonly text: {
columnType: "text";
schema: Schema.Codec<string, string, never, never>;
default: None<never>;
nullable: false;
primaryKey: false;