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", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}, false>;
}
events
= {
todoCreated: EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>
todoCreated
:
import Events
Events
.
synced<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}>(args: {
name: "v1.TodoCreated";
schema: Schema.Schema<{
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, never>;
} & Omit<DefineEventOptions<{
readonly id: string;
readonly text: string;
}, false>, "derived" | "clientOnly">): EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>
export synced
synced
({
name: "v1.TodoCreated"
name
: 'v1.TodoCreated',
schema: Schema.Schema<{
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, never>
schema
:
import Schema
Schema
.
function Struct<{
id: typeof Schema.String;
text: typeof Schema.String;
}>(fields: {
id: typeof Schema.String;
text: typeof Schema.String;
}): Schema.Struct<{
id: typeof Schema.String;
text: typeof Schema.String;
}> (+1 overload)

@since3.10.0

Struct
({
id: typeof Schema.String
id
:
import Schema
Schema
.
class String
export String

@since3.10.0

String
,
text: typeof Schema.String
text
:
import Schema
Schema
.
class String
export String

@since3.10.0

String
}),
}),
todoCompleted: EventDef<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}, false>
todoCompleted
:
import Events
Events
.
synced<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}>(args: {
name: "v1.TodoCompleted";
schema: Schema.Schema<{
readonly id: string;
}, {
readonly id: string;
}, never>;
} & Omit<DefineEventOptions<{
readonly id: string;
}, false>, "derived" | "clientOnly">): EventDef<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}, false>
export synced
synced
({
name: "v1.TodoCompleted"
name
: 'v1.TodoCompleted',
schema: Schema.Schema<{
readonly id: string;
}, {
readonly id: string;
}, never>
schema
:
import Schema
Schema
.
function Struct<{
id: typeof Schema.String;
}>(fields: {
id: typeof Schema.String;
}): Schema.Struct<{
id: typeof Schema.String;
}> (+1 overload)

@since3.10.0

Struct
({
id: typeof Schema.String
id
:
import Schema
Schema
.
class String
export String

@since3.10.0

String
}),
}),
} as
type const = {
readonly todoCreated: EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}, false>;
}
const
  • 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.

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

const schema = makeSchema({
events,
state,
unknownEventHandling: {
strategy: 'callback',
onUnknownEvent: (event, error) => {
console.warn('LiveStore saw an unknown event', { event, reason: error.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
// somewhere in your app
import type {
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>
Store
} from '@livestore/livestore'
import {
const events: {
readonly todoCreated: EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}, false>;
}
events
} from './livestore-schema.ts'
declare const
const store: Store<LiveStoreSchema.Any, {}>
store
:
class Store<TSchema extends LiveStoreSchema = LiveStoreSchema.Any, TContext = {}>
Store
const store: Store<LiveStoreSchema.Any, {}>
store
.
Store<LiveStoreSchema<TDbSchema extends DbSchema = DbSchema, TEventsDefRecord extends EventDefRecord = EventDefRecord>.Any, {}>.commit: <readonly [{
name: "v1.TodoCreated";
args: {
readonly id: string;
readonly text: string;
};
}]>(list_0: {
name: "v1.TodoCreated";
args: {
readonly id: string;
readonly text: string;
};
}) => void (+3 overloads)
commit
(
const events: {
readonly todoCreated: EventDef<"v1.TodoCreated", {
readonly id: string;
readonly text: string;
}, {
readonly id: string;
readonly text: string;
}, false>;
readonly todoCompleted: EventDef<"v1.TodoCompleted", {
readonly id: string;
}, {
readonly id: string;
}, false>;
}
events
.
todoCreated: (args: {
readonly id: string;
readonly text: string;
}) => {
name: "v1.TodoCreated";
args: {
readonly id: string;
readonly text: string;
};
}

Helper function to construct a partial event

todoCreated
({
id: string
id
: '1',
text: string
text
: 'Buy milk' }))

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: