Skip to content

Server-side clients

You can also use LiveStore on the server side e.g. via the @livestore/adapter-node adapter. This allows you to:

  • have an up-to-date server-side SQLite database (read model)
  • react to events / state changes on the server side (e.g. to send emails/push notifications)
  • commit events on the server side (e.g. for sensitive/trusted operations)

Sync Handler


Client A


Client B


Event DB

App Backend

State

(e.g. SQLite DB)

Events

(as eventlog)

pull

push

materialize

Server logic


  • send emails

  • commit trusted events

  • ...

React to events

Query

commits

events

Server-side client

LiveStore

Server-side client

Sync Handler


Client A


Client B


Event DB

App Backend

State

(e.g. SQLite DB)

Events

(as eventlog)

pull

push

materialize

Server logic


  • send emails

  • commit trusted events

  • ...

React to events

Query

commits

events

Server-side client

LiveStore

Server-side client

Note about the schema: While the events schema needs to be shared across all clients, the state schema can be different for each client (e.g. to allow for a different SQLite table design on the server side).

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

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

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

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

@example

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

@example

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

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

makeAdapter
} from '@livestore/adapter-node'
import {
const makeWsSync: (options: WsSyncOptions) => SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

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

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

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

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

@example

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

@example

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

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

makeAdapter
({
NodeAdapterOptions.storage: {
readonly type: ["in-memory"];
readonly importSnapshot?: any;
} | {
readonly type: ["fs"];
readonly baseDirectory?: string | undefined;
}
storage
: {
type: string
type
: 'fs',
baseDirectory?: string | undefined

Where to store the database files

@defaultCurrent working directory

baseDirectory
: 'tmp' },
sync?: SyncOptions
sync
: {
backend?: SyncBackendConstructor<any, JsonValue>
backend
:
function makeWsSync(options: WsSyncOptions): SyncBackendConstructor<SyncMetadata>

Creates a sync backend that uses WebSocket to communicate with the sync backend.

@example

import { makeWsSync } from '@livestore/sync-cf/client'
const syncBackend = makeWsSync({ url: 'wss://sync.example.com' })

makeWsSync
({
WsSyncOptions.url: string

URL of the sync backend

The protocol can either http/https or ws/wss

url
: 'ws://localhost:8787' }),
onSyncError?: "shutdown" | "ignore"

What to do if there is an error during sync.

Options: shutdown will stop the sync processor and cause the app to crash. ignore will log the error and let the app continue running acting as if it was offline.

@default'ignore'

onSyncError
: 'shutdown' },
})
const
const store: Store<any, {}>
store
= await
createStorePromise<any, {}, Codec<Json, Json, never, never>>({ signal, otelOptions, ...options }: CreateStoreOptionsPromise<any, {}, Codec<Json, Json, never, never>>): Promise<Store<any, {}>>

Create a new LiveStore Store

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

Adapter used for data storage and synchronization.

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

The LiveStore schema defining tables, events, and materializers.

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

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

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

storeId
: 'test',
CreateStoreOptions<any, {}, Codec<Json, Json, never, never>>.syncPayload?: Json

Payload that is sent to the sync backend when connecting

  • Its TypeScript type is inferred from syncPayloadSchema (i.e. typeof SyncPayload.Type).
  • At runtime this value is encoded with syncPayloadSchema and carried through the adapter to the backend where it can be decoded with the same schema.

@defaultundefined

syncPayload
: {
authToken: string
authToken
: 'insecure-token-change-me' },
})
const
const todos: unknown
todos
=
const store: Store<any, {}>
store
.
Store<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
todos
.
any
where
({
completed: boolean
completed
: false }))
  • The @livestore/adapter-node adapter doesn’t yet work with Cloudflare Workers but you can follow this issue for a Cloudflare adapter to enable this use case.
  • Having a @livestore/adapter-cf-worker adapter could enable serverless server-side client scenarios.