List Ordering
Fractional indexing enables conflict-free list ordering in distributed systems by assigning string-based position values that maintain lexicographic order. This makes it ideal for implementing drag-and-drop reordering in LiveStore applications where multiple clients may reorder items concurrently.
To understand how the algorithm works, see these visual and interactive explanations:
- CRDT Fractional Indexing by Evan Wallace - Visual explanation of the algorithm
- Implementing Fractional Indexing by David Greenspan - Interactive notebook walkthrough
Why Fractional Indexing?
Section titled “Why Fractional Indexing?”Traditional numeric ordering (1, 2, 3…) requires renumbering multiple items when inserting or reordering, which creates conflicts in distributed systems. Fractional indexing solves this by:
- Generating position values that can always be inserted between any two existing positions
- Using lexicographic string ordering that works naturally with SQL
ORDER BY - Eliminating the need for coordination between clients when reordering
- Avoiding cascading updates when inserting items
For example, inserting a new item between positions “a0” and “b0” generates “aV”, which sorts correctly without modifying existing items.
Installation
Section titled “Installation”Install the fractional-indexing package from Rocicorp (the same team behind Replicache):
pnpm install fractional-indexingSchema Design
Section titled “Schema Design”Define a text column to store the fractional index and add a database index for efficient ordering:
import { import State
State } from '@livestore/livestore'
export const const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task = import State
State.import SQLite
SQLite.function table<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}, { ...;}>(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:
- Using explicit column definitions
- Using an Effect Schema (either the
name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columnsconst 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 annotationsimport { 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 nameconst 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 indexesconst 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: "task"
name: 'task', columns: { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}
columns: { id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;}
id: import State
State.import SQLite
SQLite.const integer: <number, number, false, typeof NoDefault, true, false>(args: { schema?: Codec<number, number, never, never>; default?: typeof NoDefault; nullable?: false; primaryKey?: true; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;} (+1 overload)
integer({ primaryKey?: true
primaryKey: true }), title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
title: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ default?: ""
default: '' }), completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;}
completed: import State
State.import SQLite
SQLite.const integer: <number, number, false, 0, false, false>(args: { schema?: Codec<number, number, never, never>; default?: 0; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
integer({ default?: 0
default: 0 }), /** Fractional index for ordering tasks in the list */ order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
Fractional index for ordering tasks in the list
order: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ nullable?: false
nullable: false, default?: ""
default: '' }), }, indexes?: [{ readonly name: "task_order"; readonly columns: readonly ["order"];}]
indexes: [ /** Index for efficient ordering queries */ { name: "task_order"
name: 'task_order', columns: readonly ["order"]
columns: ['order'] }, ], deriveEvents?: true
deriveEvents: true,})
export type type Task = { readonly id: number; readonly title: string; readonly completed: number; readonly order: string;}
Task = typeof const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task.type Type: Struct.ReadonlySide<{ readonly id: Codec<number, number, never, never>; readonly title: Codec<string, string, never, never>; readonly completed: Codec<number, number, never, never>; readonly order: Codec<string, string, never, never>;}, "Type">
Type
export const const tables: { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; }>, State.SQLite.WithDefaults<...>, Struct<...>>;}
tables = { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task }The order column stores string values like “a0”, “a1”, “aV” that maintain lexicographic ordering. Adding a database index on this column ensures efficient queries when retrieving ordered lists.
Creating Ordered Items
Section titled “Creating Ordered Items”Use generateKeyBetween(a, b) to generate position values when creating new items:
import { function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string
generateKeyBetween } from 'fractional-indexing'
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.
Store } from '@livestore/livestore'
import { import events
events } from './events.ts'import { import tables
tables } 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.
Store
/** Create a new task at the end of the list */export const const createTaskAtEnd: (title: string) => string
Create a new task at the end of the list
createTaskAtEnd = (title: string
title: string) => { // Get the highest order value const const highestOrder: any
highestOrder = 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 tables
tables.any
task.any
select('order').any
orderBy('order', 'desc').any
limit(1))[0] ?? null
// Generate new order after the highest const const order: string
order = function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string
generateKeyBetween(const highestOrder: any
highestOrder, null)
// Commit the event 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
createTask({ title: string
title, order: string
order }))
return const order: string
order}
/** Create a new task at the beginning of the list */export const const createTaskAtStart: (title: string) => string
Create a new task at the beginning of the list
createTaskAtStart = (title: string
title: string) => { // Get the lowest order value const const lowestOrder: any
lowestOrder = 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 tables
tables.any
task.any
select('order').any
orderBy('order', 'asc').any
limit(1))[0] ?? null
// Generate new order before the lowest const const order: string
order = function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string
generateKeyBetween(null, const lowestOrder: any
lowestOrder)
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
createTask({ title: string
title, order: string
order }))
return const order: string
order}
/** Create the very first task in an empty list */export const const createFirstTask: (title: string) => string
Create the very first task in an empty list
createFirstTask = (title: string
title: string) => { // When the list is empty, use a simple default value const const order: "a1"
order = 'a1'
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
createTask({ title: string
title, order: string
order }))
return const order: "a1"
order}import { import Events
Events, import Schema
Schema } from '@livestore/livestore'
export const const events: { createTask: EventDef<"v1.CreateTask", Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String; }, "Encoded">>; updateTaskOrder: EventDef<"v1.UpdateTaskOrder", Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String; }, "Encoded">>;}
events = { createTask: EventDef<"v1.CreateTask", Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Encoded">>
createTask: import Events
Events.synced<"v1.CreateTask", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Encoded">>(args: { name: "v1.CreateTask"; schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: 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.
synced({ name: "v1.CreateTask"
name: 'v1.CreateTask', schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Encoded">, never, never>
schema: import Schema
Schema.function Struct<{ readonly title: Schema.String; readonly order: Schema.String;}>(fields: { readonly title: Schema.String; readonly order: Schema.String;}): Schema.Struct<{ readonly title: Schema.String; readonly order: 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 }
Struct({ title: Schema.String
title: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, order: Schema.String
order: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, }), }), updateTaskOrder: EventDef<"v1.UpdateTaskOrder", Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Encoded">>
updateTaskOrder: import Events
Events.synced<"v1.UpdateTaskOrder", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Encoded">>(args: { name: "v1.UpdateTaskOrder"; schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: 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.
synced({ name: "v1.UpdateTaskOrder"
name: 'v1.UpdateTaskOrder', schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Encoded">, never, never>
schema: import Schema
Schema.function Struct<{ readonly id: Schema.Finite; readonly order: Schema.String;}>(fields: { readonly id: Schema.Finite; readonly order: Schema.String;}): Schema.Struct<{ readonly id: Schema.Finite; readonly order: 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 }
Struct({ id: Schema.Finite
id: import Schema
Schema.const Finite: Schema.Finite
Type-level representation of
Finite
.
Schema for finite numbers, rejecting NaN, Infinity, and -Infinity.
Finite, order: Schema.String
order: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, }), }),}import { import State
State } from '@livestore/livestore'
export const const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task = import State
State.import SQLite
SQLite.function table<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}, { ...;}>(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:
- Using explicit column definitions
- Using an Effect Schema (either the
name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columnsconst 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 annotationsimport { 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 nameconst 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 indexesconst 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: "task"
name: 'task', columns: { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}
columns: { id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;}
id: import State
State.import SQLite
SQLite.const integer: <number, number, false, typeof NoDefault, true, false>(args: { schema?: Codec<number, number, never, never>; default?: typeof NoDefault; nullable?: false; primaryKey?: true; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;} (+1 overload)
integer({ primaryKey?: true
primaryKey: true }), title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
title: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ default?: ""
default: '' }), completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;}
completed: import State
State.import SQLite
SQLite.const integer: <number, number, false, 0, false, false>(args: { schema?: Codec<number, number, never, never>; default?: 0; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
integer({ default?: 0
default: 0 }), /** Fractional index for ordering tasks in the list */ order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
Fractional index for ordering tasks in the list
order: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ nullable?: false
nullable: false, default?: ""
default: '' }), }, indexes?: [{ readonly name: "task_order"; readonly columns: readonly ["order"];}]
indexes: [ /** Index for efficient ordering queries */ { name: "task_order"
name: 'task_order', columns: readonly ["order"]
columns: ['order'] }, ], deriveEvents?: true
deriveEvents: true,})
export type type Task = { readonly id: number; readonly title: string; readonly completed: number; readonly order: string;}
Task = typeof const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task.type Type: Struct.ReadonlySide<{ readonly id: Codec<number, number, never, never>; readonly title: Codec<string, string, never, never>; readonly completed: Codec<number, number, never, never>; readonly order: Codec<string, string, never, never>;}, "Type">
Type
export const const tables: { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; }>, State.SQLite.WithDefaults<...>, Struct<...>>;}
tables = { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task }Key patterns:
generateKeyBetween(highest, null)- Append to end of listgenerateKeyBetween(null, lowest)- Prepend to start of list- Use
"a1"as a simple default for the first item in an empty list
Reordering Items
Section titled “Reordering Items”Handle drag-and-drop by generating a new position between the target boundaries:
import { function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string
generateKeyBetween } from 'fractional-indexing'
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.
Store } from '@livestore/livestore'
import { import events
events } from './events.ts'import { import tables
tables } 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.
Store
/** * Reorder a task by moving it between two other tasks * * @param taskId - The task to reorder * @param beforeOrder - The order value of the task that will be before this one (null if moving to end) * @param afterOrder - The order value of the task that will be after this one (null if moving to start) */export const const reorderTask: (taskId: number, beforeOrder: string | null, afterOrder: string | null) => string
Reorder a task by moving it between two other tasks
reorderTask = (taskId: number
- The task to reorder
taskId: number, beforeOrder: string | null
- The order value of the task that will be before this one (null if moving to end)
beforeOrder: string | null, afterOrder: string | null
- The order value of the task that will be after this one (null if moving to start)
afterOrder: string | null) => { // Generate a new fractional index between the two positions const const newOrder: string
newOrder = function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string
generateKeyBetween(beforeOrder: string | null
- The order value of the task that will be before this one (null if moving to end)
beforeOrder, afterOrder: string | null
- The order value of the task that will be after this one (null if moving to start)
afterOrder)
// Commit the update event 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
updateTaskOrder({ id: number
id: taskId: number
- The task to reorder
taskId, order: string
order: const newOrder: string
newOrder }))
return const newOrder: string
newOrder}
/** * Handle drag-and-drop reordering * * This is a more complete example showing how to handle drag-and-drop * with proper boundary checks. */export const const handleDragDrop: (draggedTaskId: number, targetTaskId: number, dropPosition: "before" | "after") => string
Handle drag-and-drop reordering
This is a more complete example showing how to handle drag-and-drop
with proper boundary checks.
handleDragDrop = (draggedTaskId: number
draggedTaskId: number, targetTaskId: number
targetTaskId: number, dropPosition: "before" | "after"
dropPosition: 'before' | 'after') => { const const before: boolean
before = dropPosition: "before" | "after"
dropPosition === 'before'
// Get the target task's order const const targetOrder: unknown
targetOrder = 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 tables
tables.any
task.any
select('order').any
where({ id: number
id: targetTaskId: number
targetTaskId }).any
first({ behaviour: string
behaviour: 'error' }))
// Find the nearest task in the drop direction const const nearestOrder: any
nearestOrder = 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 tables
tables.any
task .any
select('order') .any
where({ order: { op: string; value: unknown;}
order: { op: string
op: const before: boolean
before === true ? '>' : '<', value: unknown
value: const targetOrder: unknown
targetOrder }, }) .any
orderBy('order', const before: boolean
before === true ? 'asc' : 'desc') .any
limit(1), )[0] ?? null
// Generate new order between target and nearest const const newOrder: string
newOrder = function generateKeyBetween(a: string | null | undefined, b: string | null | undefined, digits?: string | undefined): string
generateKeyBetween( const before: boolean
before === true ? const targetOrder: unknown
targetOrder : const nearestOrder: any
nearestOrder, const before: boolean
before === true ? const nearestOrder: any
nearestOrder : const targetOrder: unknown
targetOrder, )
// Commit the update 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
updateTaskOrder({ id: number
id: draggedTaskId: number
draggedTaskId, order: string
order: const newOrder: string
newOrder }))
return const newOrder: string
newOrder}import { import Events
Events, import Schema
Schema } from '@livestore/livestore'
export const const events: { createTask: EventDef<"v1.CreateTask", Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String; }, "Encoded">>; updateTaskOrder: EventDef<"v1.UpdateTaskOrder", Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String; }, "Encoded">>;}
events = { createTask: EventDef<"v1.CreateTask", Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Encoded">>
createTask: import Events
Events.synced<"v1.CreateTask", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Encoded">>(args: { name: "v1.CreateTask"; schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: 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.
synced({ name: "v1.CreateTask"
name: 'v1.CreateTask', schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly title: Schema.String; readonly order: Schema.String;}, "Encoded">, never, never>
schema: import Schema
Schema.function Struct<{ readonly title: Schema.String; readonly order: Schema.String;}>(fields: { readonly title: Schema.String; readonly order: Schema.String;}): Schema.Struct<{ readonly title: Schema.String; readonly order: 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 }
Struct({ title: Schema.String
title: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, order: Schema.String
order: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, }), }), updateTaskOrder: EventDef<"v1.UpdateTaskOrder", Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Encoded">>
updateTaskOrder: import Events
Events.synced<"v1.UpdateTaskOrder", Schema.Struct<Fields extends Schema.Struct.Fields>.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Encoded">>(args: { name: "v1.UpdateTaskOrder"; schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String; }, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: 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.
synced({ name: "v1.UpdateTaskOrder"
name: 'v1.UpdateTaskOrder', schema: Schema.Codec<Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Type">, Schema.Struct.ReadonlySide<{ readonly id: Schema.Finite; readonly order: Schema.String;}, "Encoded">, never, never>
schema: import Schema
Schema.function Struct<{ readonly id: Schema.Finite; readonly order: Schema.String;}>(fields: { readonly id: Schema.Finite; readonly order: Schema.String;}): Schema.Struct<{ readonly id: Schema.Finite; readonly order: 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 }
Struct({ id: Schema.Finite
id: import Schema
Schema.const Finite: Schema.Finite
Type-level representation of
Finite
.
Schema for finite numbers, rejecting NaN, Infinity, and -Infinity.
Finite, order: Schema.String
order: import Schema
Schema.const String: Schema.String
Type-level representation of
String
.
Schema for string values. Validates that the input is typeof "string".
String, }), }),}import { import State
State } from '@livestore/livestore'
export const const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task = import State
State.import SQLite
SQLite.function table<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}, { ...;}>(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:
- Using explicit column definitions
- Using an Effect Schema (either the
name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columnsconst 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 annotationsimport { 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 nameconst 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 indexesconst 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: "task"
name: 'task', columns: { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}
columns: { id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;}
id: import State
State.import SQLite
SQLite.const integer: <number, number, false, typeof NoDefault, true, false>(args: { schema?: Codec<number, number, never, never>; default?: typeof NoDefault; nullable?: false; primaryKey?: true; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;} (+1 overload)
integer({ primaryKey?: true
primaryKey: true }), title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
title: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ default?: ""
default: '' }), completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;}
completed: import State
State.import SQLite
SQLite.const integer: <number, number, false, 0, false, false>(args: { schema?: Codec<number, number, never, never>; default?: 0; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
integer({ default?: 0
default: 0 }), /** Fractional index for ordering tasks in the list */ order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
Fractional index for ordering tasks in the list
order: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ nullable?: false
nullable: false, default?: ""
default: '' }), }, indexes?: [{ readonly name: "task_order"; readonly columns: readonly ["order"];}]
indexes: [ /** Index for efficient ordering queries */ { name: "task_order"
name: 'task_order', columns: readonly ["order"]
columns: ['order'] }, ], deriveEvents?: true
deriveEvents: true,})
export type type Task = { readonly id: number; readonly title: string; readonly completed: number; readonly order: string;}
Task = typeof const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task.type Type: Struct.ReadonlySide<{ readonly id: Codec<number, number, never, never>; readonly title: Codec<string, string, never, never>; readonly completed: Codec<number, number, never, never>; readonly order: Codec<string, string, never, never>;}, "Type">
Type
export const const tables: { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; }>, State.SQLite.WithDefaults<...>, Struct<...>>;}
tables = { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task }The generateKeyBetween function automatically creates a string value that sorts lexicographically between the two boundary positions. Pass null to represent the start or end of the list.
Querying Ordered Lists
Section titled “Querying Ordered Lists”Query items using standard SQL ordering on the fractional index column:
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.
Store } from '@livestore/livestore'
import { import tables
tables } 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.
Store
/** * Query tasks in their proper order * * The fractional index values maintain lexicographic ordering, * so we can simply order by the 'order' column. */export const const getOrderedTasks: () => unknown
Query tasks in their proper order
The fractional index values maintain lexicographic ordering,
so we can simply order by the 'order' column.
getOrderedTasks = () => { return 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 tables
tables.any
task.any
select().any
orderBy('order', 'asc'))}
/** * Get the highest order value (for appending new items) */export const const getHighestOrder: () => string | null
Get the highest order value (for appending new items)
getHighestOrder = (): string | null => { const const order: any
order = 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 tables
tables.any
task.any
select('order').any
orderBy('order', 'desc').any
limit(1))[0]
return const order: any
order ?? null}
/** * Get the lowest order value (for prepending new items) */export const const getLowestOrder: () => string | null
Get the lowest order value (for prepending new items)
getLowestOrder = (): string | null => { const const order: any
order = 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 tables
tables.any
task.any
select('order').any
orderBy('order', 'asc').any
limit(1))[0]
return const order: any
order ?? null}import { import State
State } from '@livestore/livestore'
export const const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task = import State
State.import SQLite
SQLite.function table<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}, { ...;}>(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:
- Using explicit column definitions
- Using an Effect Schema (either the
name property needs to be provided or the schema needs to have a title/identifier)
// Using explicit columnsconst 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 annotationsimport { 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 nameconst 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 indexesconst 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: "task"
name: 'task', columns: { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}
columns: { id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;}
id: import State
State.import SQLite
SQLite.const integer: <number, number, false, typeof NoDefault, true, false>(args: { schema?: Codec<number, number, never, never>; default?: typeof NoDefault; nullable?: false; primaryKey?: true; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false;} (+1 overload)
integer({ primaryKey?: true
primaryKey: true }), title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
title: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ default?: ""
default: '' }), completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;}
completed: import State
State.import SQLite
SQLite.const integer: <number, number, false, 0, false, false>(args: { schema?: Codec<number, number, never, never>; default?: 0; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
integer({ default?: 0
default: 0 }), /** Fractional index for ordering tasks in the list */ order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;}
Fractional index for ordering tasks in the list
order: import State
State.import SQLite
SQLite.const text: <string, string, false, "", false, false>(args: { schema?: Codec<string, string, never, never>; default?: ""; nullable?: false; primaryKey?: false; autoIncrement?: false;}) => { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false;} (+1 overload)
text({ nullable?: false
nullable: false, default?: ""
default: '' }), }, indexes?: [{ readonly name: "task_order"; readonly columns: readonly ["order"];}]
indexes: [ /** Index for efficient ordering queries */ { name: "task_order"
name: 'task_order', columns: readonly ["order"]
columns: ['order'] }, ], deriveEvents?: true
deriveEvents: true,})
export type type Task = { readonly id: number; readonly title: string; readonly completed: number; readonly order: string;}
Task = typeof const task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task.type Type: Struct.ReadonlySide<{ readonly id: Codec<number, number, never, never>; readonly title: Codec<string, string, never, never>; readonly completed: Codec<number, number, never, never>; readonly order: Codec<string, string, never, never>;}, "Type">
Type
export const const tables: { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; }>, State.SQLite.WithDefaults<...>, Struct<...>>;}
tables = { task: State.SQLite.TableDef<State.SQLite.SqliteTableDefForInput<"task", { readonly id: { columnType: "integer"; schema: Codec<number, number, never, never>; default: None<never>; nullable: false; primaryKey: true; autoIncrement: false; }; readonly title: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; }; readonly completed: { columnType: "integer"; schema: Codec<number, number, never, never>; default: Some<0>; nullable: false; primaryKey: false; autoIncrement: false; }; readonly order: { columnType: "text"; schema: Codec<string, string, never, never>; default: Some<"">; nullable: false; primaryKey: false; autoIncrement: false; };}>, State.SQLite.WithDefaults<...>, Struct<...>>
task }Since fractional index values maintain lexicographic ordering, you can use simple ORDER BY clauses without special handling.
Best Practices
Section titled “Best Practices”- Use text/string columns - Fractional index values are strings, not numbers
- Add database indexes - Index the order column for efficient queries
- Validate lexicographic ordering - Ensure your sorting logic uses standard string comparison, not locale-aware comparison (avoid
String.prototype.localeCompare()) - Handle empty lists - Use a simple default like
"a1"for the first item - Consider bulk operations - For creating multiple items at once, use
generateNKeysBetween(a, b, n)from the same package
Real-World Example
Section titled “Real-World Example”The web-linearlite example demonstrates fractional indexing for Kanban board ordering. It shows:
- Schema definition with
kanbanordercolumn - Creating issues with proper ordering
- Drag-and-drop reordering across columns
- Querying issues in order by status
Check examples/web-linearlite/src/livestore/schema/issue.ts for the schema and examples/web-linearlite/src/components/column.tsx for the drag-and-drop implementation.
How It Works
Section titled “How It Works”Fractional indexing generates position strings that always allow insertion between any two positions:
Initial: a0 a1 a2Insert between a0 and a1: a0V (sorts: a0 < a0V < a1)Insert between a0V and a1: a0n (sorts: a0 < a0V < a0n < a1)The algorithm ensures:
- Position strings stay reasonably short
- No coordination needed between clients
- Conflicts resolve naturally through lexicographic ordering
- Works seamlessly with LiveStore’s event sourcing model
For more details on the algorithm, see the fractional-indexing documentation.