Skip to content

Events

There are two types of events:

  • synced: Events that are synced across clients
  • clientOnly: Events that are only processed locally on the client (but still synced across client sessions e.g. across browser tabs/windows)

An event definition consists of a unique name of the event and a schema for the event arguments. It’s recommended to version event definitions to make it easier to evolve them over time.

Events will be synced across clients and materialized into state (i.e. SQLite tables) via materializers.

// livestore/schema.ts
import {
import Events
Events
,
import Schema
Schema
} from '@livestore/livestore'
export const
const events: {
readonly todoCreated: 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: EventDef<"v1.TodoCompleted", Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">>;
}
events
= {
todoCreated: 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<...>): 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
}),
}),
todoCompleted: EventDef<"v1.TodoCompleted", Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">>
todoCompleted
:
import Events
Events
.
synced<"v1.TodoCompleted", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">>(args: {
name: "v1.TodoCompleted";
schema: Schema.Codec<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">, never, never>;
} & Omit<DefineEventOptions<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, false>, "derived" | "clientOnly">): 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.TodoCompleted"
name
: 'v1.TodoCompleted',
schema: Schema.Codec<Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">, never, never>
schema
:
import Schema
Schema
.
function Struct<{
readonly id: Schema.String;
}>(fields: {
readonly id: Schema.String;
}): Schema.Struct<{
readonly id: 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
}),
}),
} as
type const = {
readonly todoCreated: 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: EventDef<"v1.TodoCompleted", Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Type">, Schema.Struct.ReadonlySide<{
readonly id: Schema.String;
}, "Encoded">>;
}
const
// somewhere in your app
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 events
events
} from './livestore-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 events
events
.
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
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
)
}
  • It’s strongly recommended to use past-tense event names (e.g. todoCreated/createdTodo instead of todoCreate/createTodo) to indicate something already occurred.
  • When generating IDs for events (e.g. for the todo in the example above), it’s recommended to use a globally unique ID generator (e.g. UUID, nanoid, etc.) to avoid conflicts. For convenience, @livestore/livestore re-exports the nanoid function.
  • TODO: write down more best practices
  • TODO: mention AI linting (either manually or via a CI step)
    • core idea: feed list of best practices to AI and check if events adhere to them + get suggestions if not
  • It’s recommended to avoid DELETE events and instead use soft-deletes (e.g. add a deleted date/boolean column with a default value of null). This helps avoid some common concurrency issues.

Client A

Client session A2

Client session A1



Client leader A


sync Backend



Client B

Client C

Event Log

Event Log

Materialized State

Materialized State

SyncState

Materialized State

SyncState

Client A

Client session A2

Client session A1



Client leader A


sync Backend



Client B

Client C

Event Log

Event Log

Materialized State

Materialized State

SyncState

Materialized State

SyncState

  • SyncState: in-memory for pending events only
  • dbState: Materialized state that matches the schema (SQLite database)
  • dbEventLog: Database that stores the durable event log and tracks global sequence of events which the backend has acknowledged
  • dbState: Materialized state that matches the schema (SQLite database) so leader can materialize events and handle rollbacks
  • EventLog: Any storage solution that supports pushing and pulling events. Append only storage.
  1. Client session commits an event

    • Client session merges the new event e3 into its local SyncState as pending
    • Client session pushes the pending event to the client leader thread; the leader still shows the previous head until it persists the event
      Client Session
      e1
      e2
      e3'
      Client Leader
      e1
      e2
      Sync Backend
      e1
      e2
  2. Client leader persists the event

    • Client leader materializes the event and writes it to EventLog
    • Event e3 remains unconfirmed from the leader’s perspective because the backend has not acknowledged it yet
      Client Session
      e1
      e2
      e3'
      Client Leader
      e1
      e2
      e3'
      Sync Backend
      e1
      e2
  3. Leader thread emits signal back to subscribed clients

    • Client session merges the authoritative event from the leader
    • Event transitions from pending to confirmed on the client while the leader still waits for backend confirmation
      Client Session
      e1
      e2
      e3
      Client Leader
      e1
      e2
      e3'
      Sync Backend
      e1
      e2
  4. Leader thread pushes the event to the sync backend

    • Leader pushes the pending event upstream; it stays marked as unconfirmed in the client leader’s eventlog until the backend acknowledges receipt
      Client Session
      e1
      e2
      e3
      Client Leader
      e1
      e2
      e3'
      Sync Backend
      e1
      e2
  5. Sync backend pulls the event from the client leader

    • Sync backend acknowledges the event and advances its head
    • Client leader receives the acknowledgement and marks the event as confirmed
    • All heads align on the confirmed sequence
      Client Session
      e1
      e2
      e3
      Client Leader
      e1
      e2
      e3
      Sync Backend
      e1
      e2
      e3

This example shows how a client session rebases its pending events when new authoritative events arrive from upstream. Client A owns the local work that gets rebased, while client B introduces the authoritative change. Colors follow the client IDs so lineage remains visible, and origin notation tracks the rebased event.

  1. Client session has local pending work while upstream advances

    • Client session holds pending event A:e3'{todoRenamed} built on top of shared history e1 → e2
    • Sync backend publishes authoritative event B:e3{todoRenamed} that replaces the client’s local change
      Client Session
      e1
      e2
      e3
      Client Leader
      e1
      e2
      e3'
      Sync Backend
      e1
      e2
      e3
  2. Client leader pulls authoritative events from the sync backend

    • Client compares its pending chain with upstream events and spots the divergence at e2
    • Client rolls back events and state to the point of divergence
      Client Session
      e1
      e2
      Client Leader
      e1
      e2
      Sync Backend
      e1
      e2
      e3
  3. Client applies authoritative upstream events

    • Client session and leader apply the authoritative upstream events and advances their heads to e3
      Client Session
      e1
      e2
      e3
      Client Leader
      e1
      e2
      e3
      Sync Backend
      e1
      e2
      e3
  4. Client replays its local pending events on top of the new head

    • Stored original events keep their payload but their sequence number gets updated to follow upstream head
    • Each newly numbered event is re-appplied and materialized to state in both client session and client leader
      Client Session
      e1
      e2
      e3
      e4
      Client Leader
      e1
      e2
      e3
      e4'
      Sync Backend
      e1
      e2
      e3
  5. Client pushes its local pending events to sync backend

    • Upon receipt local pending events are marked as confirmed and the client leader advances its head to e4
      Client Session
      e1
      e2
      e3
      e4
      Client Leader
      e1
      e2
      e3
      e4
      Sync Backend
      e1
      e2
      e3
      e4

Older clients might receive events that were introduced in newer app versions. Configure the behaviour centrally via unknownEventHandling when constructing the schema:

const
const _schema: FromInputSchema.DeriveSchema<{
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">>;
};
state: InternalState;
unknownEventHandling: {
strategy: "callback";
onUnknownEvent: (event: UnknownEventContext, error: UnknownEventError) => void;
};
}>
_schema
=
makeSchema<{
events: {
readonly todoCreated: State.SQLite.EventDef<"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">>;
};
state: InternalState;
unknownEventHandling: {
strategy: "callback";
onUnknownEvent: (event: UnknownEventContext, error: UnknownEventError) => void;
};
}>(inputSchema: {
events: {
readonly todoCreated: State.SQLite.EventDef<"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">>;
};
state: InternalState;
unknownEventHandling: {
strategy: "callback";
onUnknownEvent: (event: UnknownEventContext, error: UnknownEventError) => void;
};
}): FromInputSchema.DeriveSchema<...>
makeSchema
({
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">>;
}
events
,
state: InternalState
state
,
unknownEventHandling: {
strategy: "callback";
onUnknownEvent: (event: UnknownEventContext, error: UnknownEventError) => void;
}
unknownEventHandling
: {
strategy: "callback"
strategy
: 'callback',
onUnknownEvent: (event: UnknownEventContext, error: UnknownEventError) => void
onUnknownEvent
: (
event: UnknownEventContext
event
,
error: UnknownEventError
error
) => {
var console: Console
console
.
Console.warn(...data: any[]): void (+2 overloads)

The console.warn() static method outputs a warning message to the console at the 'warning' log level.

MDN Reference

warn
('LiveStore saw an unknown event', {
event: UnknownEventContext
event
,
reason: "event-definition-missing" | "materializer-missing"
reason
:
error: UnknownEventError
error
.
reason: "event-definition-missing" | "materializer-missing"
reason
})
},
},
})

Pick 'warn' (default) to log every occurrence, 'ignore' to silently drop new events until the client updates, 'fail' to halt immediately, or 'callback' to delegate to custom logging/telemetry while continuing to process the log.

  • Event definitions can’t be removed after they were added to your app.
  • Event schema definitions can be evolved as long as the changes are forward-compatible.
    • That means data encoded with the old schema can be decoded with the new schema.
    • In practice, this means …
      • for structs …
        • you can add new fields if they have default values or are optional
        • you can remove fields

Each event has the following structure:

FieldDescription
nameEvent name matching the event definition
argsEvent arguments as defined by the event schema
seqNumSequence number identifying this event
parentSeqNumParent event’s sequence number (for causal ordering)
clientIdIdentifier of the client that created the event
sessionIdIdentifier of the session

Events exist in two formats:

Decoded - Native TypeScript types used in application code:

{
"name": "todoCreated-v1",
"args": { "id": "abc123", "text": "Buy milk", "createdAt": Date },
"seqNum": 5,
"parentSeqNum": 4,
"clientId": "client-xyz",
"sessionId": "session-123"
}

Encoded - Serialized format for storage and sync:

{
"name": "todoCreated-v1",
"args": { "id": "abc123", "text": "Buy milk", "createdAt": "2024-01-15T10:30:00.000Z" },
"seqNum": 5,
"parentSeqNum": 4,
"clientId": "client-xyz",
"sessionId": "session-123"
}

The args field is encoded according to the event’s schema (e.g., Date objects become ISO strings, binary data becomes base64). LiveStore handles encoding/decoding automatically.

On the client, sequence numbers are expanded to track additional information for local events:

{
"seqNum": { "global": 5, "client": 1, "rebaseGeneration": 0 },
"parentSeqNum": { "global": 5, "client": 0, "rebaseGeneration": 0 }
}
  • global: Globally unique integer assigned by the sync backend (EventSequenceNumber.Global)
  • client: Client-local counter (0 for synced events, increments for client-only events) (EventSequenceNumber.Client)
  • rebaseGeneration: Increments when the client rebases unconfirmed events

Events can be represented as strings like e5 (global event 5), e5.1 (client-local event), or e5r1 (after a rebase).

For the full type definitions, see LiveStoreEvent and EventSequenceNumber.

LiveStore organizes event types into namespaces based on their usage context:

NamespaceDescriptionSequence Number Format
LiveStoreEvent.InputEvents without sequence numbers (for committing)None
LiveStoreEvent.GlobalSync backend formatInteger (seqNum: number)
LiveStoreEvent.ClientClient-side format with full metadataStruct (seqNum: { global, client, rebaseGeneration })
import {
import EventSequenceNumber
EventSequenceNumber
, type
import LiveStoreEvent
LiveStoreEvent
} from '@livestore/livestore'
// Input events (no sequence numbers) - used when committing
const
const _input: LiveStoreEvent.Input.Decoded
_input
:
import LiveStoreEvent
LiveStoreEvent
.
import Input
Input
.
type Decoded = {
name: string;
args: any;
}

Event without sequence numbers, with decoded (native TypeScript) args.

Decoded
= {
name: string
name
: 'todoCreated-v1',
args: any
args
: {
id: string
id
: 'abc123',
text: string
text
: 'Buy milk' },
}
// Global events (sync backend format) - integer sequence numbers
const
const _global: Struct.ReadonlySide<{
readonly name: String;
readonly args: Any;
readonly seqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly parentSeqNum: brand<Int, "GlobalEventSequenceNumber">;
readonly clientId: String;
readonly sessionId: String;
}, "Type">
_global
:
import LiveStoreEvent
LiveStoreEvent
.
import Global
Global
.
type Encoded = {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}

Effect Schema for global events with integer sequence numbers.

Event with integer sequence numbers for sync backend wire format.

@example

const event: LiveStoreEvent.Global.Encoded = {
name: 'todoCreated-v1',
args: { id: 'abc', text: 'Buy milk' },
seqNum: 5, // This event's position in the global log
parentSeqNum: 4, // Points to the previous event
clientId: 'client-xyz',
sessionId: 'session-123'
}

Encoded
= {
name: string
name
: 'todoCreated-v1',
args: any
args
: {
id: string
id
: 'abc123',
text: string
text
: 'Buy milk' },
seqNum: number & Brand<"GlobalEventSequenceNumber">
seqNum
:
import EventSequenceNumber
EventSequenceNumber
.
import Global
Global
.
const make: Constructor
(unbranded: number) => EventSequenceNumber.Global.Type

Constructs a branded type from a value of type Unbranded<B>, throwing an error if the provided value is not valid.

@example

const seqNum = EventSequenceNumber.Global.make(5)

make
(5),
parentSeqNum: number & Brand<"GlobalEventSequenceNumber">
parentSeqNum
:
import EventSequenceNumber
EventSequenceNumber
.
import Global
Global
.
const make: Constructor
(unbranded: number) => EventSequenceNumber.Global.Type

Constructs a branded type from a value of type Unbranded<B>, throwing an error if the provided value is not valid.

@example

const seqNum = EventSequenceNumber.Global.make(5)

make
(4),
clientId: string
clientId
: 'client-xyz',
sessionId: string
sessionId
: 'session-123',
}
// Client events (local format) - composite sequence numbers
const
const _client: LiveStoreEvent.Client.Encoded
_client
:
import LiveStoreEvent
LiveStoreEvent
.
import Client
Client
.
type Encoded = {
name: string;
args: any;
seqNum: EventSequenceNumber.Client.Composite;
parentSeqNum: EventSequenceNumber.Client.Composite;
clientId: string;
sessionId: string;
}

Effect Schema for client events with encoded args.

Event with composite sequence numbers and encoded (serialized) args.

@example

// Confirmed event (client=0)
const event: LiveStoreEvent.Client.Encoded = {
name: 'todoCreated-v1',
args: { id: 'abc', text: 'Buy milk' },
seqNum: { global: 5, client: 0, rebaseGeneration: 0 },
parentSeqNum: { global: 4, client: 0, rebaseGeneration: 0 },
clientId: 'client-xyz',
sessionId: 'session-123'
}
// Pending local event (client=1, not yet synced)
const pending: LiveStoreEvent.Client.Encoded = {
...event,
seqNum: { global: 5, client: 1, rebaseGeneration: 0 }, // e5.1
}

Encoded
= {
name: string
name
: 'todoCreated-v1',
args: any
args
: {
id: string
id
: 'abc123',
text: string
text
: 'Buy milk' },
seqNum: EventSequenceNumber.Client.Composite
seqNum
:
import EventSequenceNumber
EventSequenceNumber
.
import Client
Client
.
const Composite: Struct<{
readonly global: brand<Int, "GlobalEventSequenceNumber">;
readonly client: brand<Int, "ClientEventSequenceNumber">;
readonly rebaseGeneration: Int;
}> & {
make: (seqNum: EventSequenceNumber.Client.CompositeInput) => EventSequenceNumber.Client.Composite;
}

Composite event sequence number consisting of global + client + rebaseGeneration. Used for client-side event tracking with support for unconfirmed local events.

For event notation documentation, see: contributor-docs/events-notation.md

Effect Schema for the composite event sequence number (global + client + rebaseGeneration). Also includes a make helper for creating validated Composite values.

@example

const seqNum: EventSequenceNumber.Client.Composite = {
global: EventSequenceNumber.Global.make(5),
client: EventSequenceNumber.Client.DEFAULT,
rebaseGeneration: 0
}
const validated = EventSequenceNumber.Client.Composite.make({ global: 5, client: 0, rebaseGeneration: 0 })

Composite
.
make: (seqNum: EventSequenceNumber.Client.CompositeInput) => EventSequenceNumber.Client.Composite (+1 overload)

Creates a validated Composite sequence number from input. If rebaseGeneration is omitted, defaults to REBASE_GENERATION_DEFAULT (0).

make
({
global: number
global
: 5,
client: number
client
: 0,
rebaseGeneration: number
rebaseGeneration
: 0 }),
parentSeqNum: EventSequenceNumber.Client.Composite
parentSeqNum
:
import EventSequenceNumber
EventSequenceNumber
.
import Client
Client
.
const Composite: Struct<{
readonly global: brand<Int, "GlobalEventSequenceNumber">;
readonly client: brand<Int, "ClientEventSequenceNumber">;
readonly rebaseGeneration: Int;
}> & {
make: (seqNum: EventSequenceNumber.Client.CompositeInput) => EventSequenceNumber.Client.Composite;
}

Composite event sequence number consisting of global + client + rebaseGeneration. Used for client-side event tracking with support for unconfirmed local events.

For event notation documentation, see: contributor-docs/events-notation.md

Effect Schema for the composite event sequence number (global + client + rebaseGeneration). Also includes a make helper for creating validated Composite values.

@example

const seqNum: EventSequenceNumber.Client.Composite = {
global: EventSequenceNumber.Global.make(5),
client: EventSequenceNumber.Client.DEFAULT,
rebaseGeneration: 0
}
const validated = EventSequenceNumber.Client.Composite.make({ global: 5, client: 0, rebaseGeneration: 0 })

Composite
.
make: (seqNum: EventSequenceNumber.Client.CompositeInput) => EventSequenceNumber.Client.Composite (+1 overload)

Creates a validated Composite sequence number from input. If rebaseGeneration is omitted, defaults to REBASE_GENERATION_DEFAULT (0).

make
({
global: number
global
: 4,
client: number
client
: 0,
rebaseGeneration: number
rebaseGeneration
: 0 }),
clientId: string
clientId
: 'client-xyz',
sessionId: string
sessionId
: 'session-123',
}

The history of all events that have been committed is stored forms the “eventlog”. It is persisted in the client as well as in the sync backend.

Example eventlog.db: