Skip to content

Auth

LiveStore doesn’t include built-in authentication or authorization support, but you can implement it in your app’s logic.

Use the syncPayload store option to send a custom payload to your sync backend.

The following example sends the authenticated user’s JWT to the server.

const
const useAppStore: () => Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi
useAppStore
= () =>
useStore<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>(options: RegistryStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>): Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi

Returns a store instance augmented with hooks (store.useQuery() and store.useClientDocument()) for reactive queries.

@example

function Issue() {
// Suspends until loaded or returns immediately if already loaded
const issueStore = useStore(issueStoreOptions('abc123'))
const [issue] = issueStore.useQuery(queryDb(tables.issue.select()))
const toggleStatus = () =>
issueStore.commit(
issueEvents.issueStatusChanged({
id: issue.id,
status: issue.status === 'done' ? 'todo' : 'done',
}),
)
const preloadParentIssue = (issueId: string) =>
storeRegistry.preload({
...issueStoreOptions(issueId),
unusedCacheTime: 10_000,
})
return (
<>
<h2>{issue.title}</h2>
<button onClick={() => toggleStatus()}>Toggle Status</button>
<button onMouseEnter={() => preloadParentIssue(issue.parentIssueId)}>Open Parent Issue</button>
</>
)
}

@returnsThe loaded store instance augmented with React hooks

@throwsunknown - store loading error or if called outside <StoreRegistryProvider>

useStore
({
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
,
CreateStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>.schema: LiveStoreSchema<DbSchema, EventDefRecord>

The LiveStore schema defining tables, events, and materializers.

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

Adapter used for data storage and synchronization.

adapter
,
CreateStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>.batchUpdates?: (run: () => void) => void

Needed in React so LiveStore can apply multiple events in a single render.

@example

// With React DOM
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
// With React Native
import { unstable_batchedUpdates as batchUpdates } from 'react-native'

batchUpdates
,
CreateStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, 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
:
const user: {
jwt: string;
}
user
.
jwt: string
jwt
, // Using a JWT
},
})
export const
const App: () => JSX.Element
App
= () => {
const [
const storeRegistry: StoreRegistry
storeRegistry
] =
useState<StoreRegistry>(initialState: StoreRegistry | (() => StoreRegistry)): [StoreRegistry, Dispatch<SetStateAction<StoreRegistry>>] (+1 overload)

Returns a stateful value, and a function to update it.

@version16.8.0

@seehttps://react.dev/reference/react/useState

useState
(() => new
new StoreRegistry(config?: StoreRegistryConfig): StoreRegistry

Creates a new StoreRegistry instance.

@example

const registry = new StoreRegistry({
defaultOptions: {
batchUpdates,
unusedCacheTime: 30_000,
}
})

StoreRegistry
())
return (
<
const StoreRegistryProvider: ({ storeRegistry, children }: StoreRegistryProviderProps) => JSX.Element

React context provider that makes a

StoreRegistry

available to descendant components.

Wrap your application (or a subtree) with this provider to enable

useStore

and

useStoreRegistry

hooks within that tree.

@example

import { StoreRegistry } from '@livestore/livestore'
import { StoreRegistryProvider } from '@livestore/react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
const storeRegistry = new StoreRegistry({
defaultOptions: { batchUpdates }
})
function App() {
return (
<StoreRegistryProvider storeRegistry={storeRegistry}>
<MyComponent />
</StoreRegistryProvider>
)
}

StoreRegistryProvider
storeRegistry: StoreRegistry
storeRegistry
={
const storeRegistry: StoreRegistry
storeRegistry
}>
<
const AppContent: () => JSX.Element
AppContent
/>
</
const StoreRegistryProvider: ({ storeRegistry, children }: StoreRegistryProviderProps) => JSX.Element

React context provider that makes a

StoreRegistry

available to descendant components.

Wrap your application (or a subtree) with this provider to enable

useStore

and

useStoreRegistry

hooks within that tree.

@example

import { StoreRegistry } from '@livestore/livestore'
import { StoreRegistryProvider } from '@livestore/react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
const storeRegistry = new StoreRegistry({
defaultOptions: { batchUpdates }
})
function App() {
return (
<StoreRegistryProvider storeRegistry={storeRegistry}>
<MyComponent />
</StoreRegistryProvider>
)
}

StoreRegistryProvider
>
</
const Suspense: ExoticComponent<SuspenseProps>

Lets you display a fallback until its children have finished loading.

@seehttps://react.dev/reference/react/Suspense React Docs

@example

import { Suspense } from 'react';
<Suspense fallback={<Loading />}>
<ProfileDetails />
</Suspense>

Suspense
>
)
}
const
const AppContent: () => JSX.Element
AppContent
= () => {
const
const _store: Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi
_store
=
const useAppStore: () => Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi
useAppStore
()
// Use the store in your components
return <
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{/* Your app content */}</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
}

On the sync server, validate the token and allow or reject the sync based on the result. See the following example:

import * as
import jose
jose
from 'jose'
import {
const makeDurableObject: MakeDurableObjectClass

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
,
const makeWorker: <TEnv extends Env = Env, TDurableObjectRpc extends Rpc.DurableObjectBranded | undefined = undefined, TSyncPayload = Json>(options: MakeWorkerOptions<TEnv, TSyncPayload>) => CFWorker<TEnv, TDurableObjectRpc>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
} from '@livestore/sync-cf/cf-worker'
const
const JWT_SECRET: "a-string-secret-at-least-256-bits-long"
JWT_SECRET
= 'a-string-secret-at-least-256-bits-long'
export class
class SyncBackendDO
SyncBackendDO
extends
function makeDurableObject(options?: MakeDurableObjectClassOptions): {
new (ctx: DoState, env: Env): DoObject<SyncBackendRpcInterface>;
}

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
({
onPush?: (message: PushRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPush
: async (
message: {
readonly batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[];
readonly backendId: Option<string>;
}
message
) => {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

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

MDN Reference

log
('onPush',
message: {
readonly batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[];
readonly backendId: Option<string>;
}
message
.
batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[]
batch
)
},
onPull?: (message: PullRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPull
: async (
message: {
readonly cursor: Option<{
readonly backendId: string;
readonly eventSequenceNumber: number & Brand<"GlobalEventSequenceNumber">;
}>;
}
message
) => {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

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

MDN Reference

log
('onPull',
message: {
readonly cursor: Option<{
readonly backendId: string;
readonly eventSequenceNumber: number & Brand<"GlobalEventSequenceNumber">;
}>;
}
message
)
},
}) {}
export default
makeWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined, any>(options: MakeWorkerOptions<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, any>): CFWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
({
syncBackendBinding: "SYNC_BACKEND_DO"

Binding name of the sync Durable Object declared in wrangler config.

syncBackendBinding
: 'SYNC_BACKEND_DO',
validatePayload?: (payload: any, context: ValidatePayloadContext) => void | Promise<void>

Validates the (optionally decoded) payload during WebSocket connection establishment. If

syncPayloadSchema

is provided, payload will be of the schema's inferred type.

The context includes request headers for cookie-based or header-based authentication.

@example

Cookie-based authentication

validatePayload: async (payload, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

Note: This runs only at connection time, not for individual push events. For push event validation, use the onPush callback in the Durable Object.

validatePayload
: async (
payload: any
payload
: any,
context: ValidatePayloadContext
context
) => {
const {
const storeId: string
storeId
} =
context: ValidatePayloadContext
context
const {
const authToken: any
authToken
} =
payload: any
payload
if (
const authToken: any
authToken
== null) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('No auth token provided')
}
const
const user: jose.JWTPayload | undefined
user
= await
const getUserFromToken: (token: string) => Promise<jose.JWTPayload | undefined>
getUserFromToken
(
const authToken: any
authToken
)
if (
const user: jose.JWTPayload | undefined
user
== null) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Invalid auth token')
} else {
// User is authenticated!
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

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

MDN Reference

log
('Sync backend payload',
var JSON: JSON

An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

JSON
.
JSON.stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string (+1 overload)

Converts a JavaScript value to a JavaScript Object Notation (JSON) string.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

@throws{TypeError} If a circular reference or a BigInt value is found.

stringify
(
const user: jose.JWTPayload
user
, null, 2))
}
// Check if token is expired
if (
payload: any
payload
.
any
exp
!==
var undefined
undefined
&&
payload: any
payload
.
any
exp
<
var Date: DateConstructor

Enables basic storage and retrieval of dates and times.

Date
.
DateConstructor.now(): number

Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).

now
() / 1000) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Token expired')
}
await
const checkUserAccess: (payload: jose.JWTPayload, storeId: string) => Promise<void>
checkUserAccess
(
const user: jose.JWTPayload
user
,
const storeId: string
storeId
)
},
enableCORS?: boolean

@defaultfalse

enableCORS
: true,
})
const
const getUserFromToken: (token: string) => Promise<jose.JWTPayload | undefined>
getUserFromToken
= async (
token: string
token
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<
import jose
jose
.
export JWTPayload

Recognized JWT Claims Set members, any other members may also be present.

JWTPayload
| undefined> => {
try {
const {
const payload: jose.JWTPayload

JWT Claims Set.

payload
} = await
import jose
jose
.
jwtVerify<jose.JWTPayload>(jwt: string | Uint8Array, key: jose.CryptoKey | jose.KeyObject | jose.JWK | Uint8Array, options?: jose.JWTVerifyOptions): Promise<jose.JWTVerifyResult<jose.JWTPayload>> (+1 overload)
export jwtVerify

Verifies the JWT format (to be a JWS Compact format), verifies the JWS signature, validates the JWT Claims Set.

This function is exported (as a named export) from the main 'jose' module entry point as well as from its subpath export 'jose/jwt/verify'.

@paramjwt JSON Web Token value (encoded as JWS).

@paramkey Key to verify the JWT with. See https://github.com/panva/jose/issues/210#jws-alg Algorithm Key Requirements.

@paramoptions JWT Decryption and JWT Claims Set validation options.

jwtVerify
(
token: string
token
, new
var TextEncoder: new () => TextEncoder

The TextEncoder interface enables you to encode a JavaScript string using UTF-8.

MDN Reference

An implementation of the WHATWG Encoding Standard TextEncoder API. All instances of TextEncoder only support UTF-8 encoding.

const encoder = new TextEncoder();
const uint8array = encoder.encode('this is some data');

TextEncoder
().
TextEncoder.encode(input?: string): Uint8Array<ArrayBuffer>

The TextEncoder.encode() method takes a string as input, and returns a Uint8Array containing the string encoded using UTF-8.

MDN Reference

encode
(
const JWT_SECRET: "a-string-secret-at-least-256-bits-long"
JWT_SECRET
))
return
const payload: jose.JWTPayload

JWT Claims Set.

payload
} catch (
function (local var) error: unknown
error
) {
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

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

MDN Reference

log
('⚠️ Error verifying token',
function (local var) error: unknown
error
)
return
var undefined
undefined
}
}
const
const checkUserAccess: (payload: jose.JWTPayload, storeId: string) => Promise<void>
checkUserAccess
= async (
payload: jose.JWTPayload
payload
:
import jose
jose
.
export JWTPayload

Recognized JWT Claims Set members, any other members may also be present.

JWTPayload
,
storeId: string
storeId
: string):
interface Promise<T>

Represents the completion of an asynchronous operation

Promise
<void> => {
// Check if user is authorized to access the store
var console: Console
console
.
Console.log(...data: any[]): void (+2 overloads)

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

MDN Reference

log
('Checking access for store',
storeId: string
storeId
, 'with payload',
payload: jose.JWTPayload
payload
)
}

The above example uses jose, a popular JavaScript module that supports JWTs. It works across various runtimes, including Node.js, Cloudflare Workers, Deno, Bun, and others.

The validatePayload function receives the authToken, checks if the payload exists, and verifies that it’s valid and hasn’t expired. If all checks pass, sync continues as normal. If any check fails, the server rejects the sync.

The client app still works as expected, but saves data locally. If the user re-authenticates or refreshes the token later, LiveStore syncs any local changes made while the user was unauthenticated.

Re-validate payload inside the Durable Object

Section titled “Re-validate payload inside the Durable Object”

When you rely on syncPayload, treat it as untrusted input. Decode the token inside validatePayload to gate the connection, and then repeat the same verification inside the Durable Object before trusting per-push metadata.

type
type SyncPayload = {
authToken?: string;
userId?: string;
}
SyncPayload
= {
authToken?: string
authToken
?: string;
userId?: string
userId
?: string }
type
type AuthorizedSession = {
authToken: string;
userId: string;
}
AuthorizedSession
= {
authToken: string
authToken
: string
userId: string
userId
: string
}
const
const ensureAuthorized: (payload: unknown) => AuthorizedSession
ensureAuthorized
= (
payload: unknown
payload
: unknown):
type AuthorizedSession = {
authToken: string;
userId: string;
}
AuthorizedSession
=> {
if (
payload: unknown
payload
===
var undefined
undefined
||
payload: {} | null
payload
=== null || typeof
payload: {}
payload
!== 'object') {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Missing auth payload')
}
const {
const authToken: string | undefined
authToken
,
const userId: string | undefined
userId
} =
payload: object
payload
as
type SyncPayload = {
authToken?: string;
userId?: string;
}
SyncPayload
if (
const authToken: string | undefined
authToken
== null) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Missing auth token')
}
const
const claims: any
claims
=
import verifyJwt
verifyJwt
(
const authToken: string
authToken
)
if (
const claims: any
claims
.
any
sub
== null) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Token missing subject claim')
}
if (
const userId: string | undefined
userId
!==
var undefined
undefined
&&
const userId: string
userId
!==
const claims: any
claims
.
any
sub
) {
throw new
var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+2 overloads)
Error
('Payload userId mismatch')
}
return {
authToken: string
authToken
,
userId: string
userId
:
const claims: any
claims
.
any
sub
}
}
export default
makeWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined, Json>(options: MakeWorkerOptions<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, Json>): CFWorker<{
SYNC_BACKEND_DO: any;
} & {
SYNC_BACKEND_DO: any;
}, undefined>

Produces a Cloudflare Worker fetch handler that delegates sync traffic to the Durable Object identified by syncBackendBinding.

For more complex setups prefer implementing a custom fetch and call

handleSyncRequest

from the branch that handles LiveStore sync requests.

makeWorker
({
syncBackendBinding: "SYNC_BACKEND_DO"

Binding name of the sync Durable Object declared in wrangler config.

syncBackendBinding
: 'SYNC_BACKEND_DO',
validatePayload?: (payload: Json, context: ValidatePayloadContext) => void | Promise<void>

Validates the (optionally decoded) payload during WebSocket connection establishment. If

syncPayloadSchema

is provided, payload will be of the schema's inferred type.

The context includes request headers for cookie-based or header-based authentication.

@example

Cookie-based authentication

validatePayload: async (payload, { storeId, headers }) => {
const cookie = headers.get('cookie')
const session = await validateSessionFromCookie(cookie)
if (!session) throw new Error('Unauthorized')
}

Note: This runs only at connection time, not for individual push events. For push event validation, use the onPush callback in the Durable Object.

validatePayload
: (
payload: Json
payload
) => {
const ensureAuthorized: (payload: unknown) => AuthorizedSession
ensureAuthorized
(
payload: Json
payload
)
},
})
export class
class SyncBackendDO
SyncBackendDO
extends
function makeDurableObject(options?: MakeDurableObjectClassOptions): {
new (ctx: DoState, env: Env): DoObject<SyncBackendRpcInterface>;
}

Creates a Durable Object class for handling WebSocket-based sync. A sync Durable Object is uniquely scoped to a specific storeId.

The sync DO supports 3 transport modes:

  • HTTP JSON-RPC
  • WebSocket
  • Durable Object RPC calls (only works in combination with @livestore/adapter-cf)

Example:

// In your Cloudflare Worker file
import { makeDurableObject } from '@livestore/sync-cf/cf-worker'
export class SyncBackendDO extends makeDurableObject({
onPush: async (message) => {
console.log('onPush', message.batch)
},
onPull: async (message) => {
console.log('onPull', message)
},
}) {}

wrangler.toml

[[durable_objects.bindings]]
name = "SYNC_BACKEND_DO"
class_name = "SyncBackendDO"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SyncBackendDO"]

makeDurableObject
({
onPush?: (message: SyncMessage.PushRequest, context: CallbackContext) => SyncOrPromiseOrEffect<void>
onPush
: async (
message: {
readonly batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[];
readonly backendId: Option<string>;
}
message
:
import SyncMessage
SyncMessage
.
type PushRequest = {
readonly batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[];
readonly backendId: Option<string>;
}
PushRequest
, {
payload: Json | undefined
payload
}) => {
const {
const userId: string
userId
} =
const ensureAuthorized: (payload: unknown) => AuthorizedSession
ensureAuthorized
(
payload: Json | undefined
payload
)
await
const ensureTenantAccess: (_userId: string, _batch: SyncMessage.PushRequest["batch"]) => Promise<void>
ensureTenantAccess
(
const userId: string
userId
,
message: {
readonly batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[];
readonly backendId: Option<string>;
}
message
.
batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[]
batch
)
},
}) {}
const
const ensureTenantAccess: (_userId: string, _batch: SyncMessage.PushRequest["batch"]) => Promise<void>
ensureTenantAccess
= async (
_userId: string
_userId
: string,
_batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[]
_batch
:
import SyncMessage
SyncMessage
.
type PushRequest = {
readonly batch: readonly {
readonly name: string;
readonly args: any;
readonly seqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly parentSeqNum: number & Brand<"GlobalEventSequenceNumber">;
readonly clientId: string;
readonly sessionId: string;
}[];
readonly backendId: Option<string>;
}
PushRequest
['batch']) => {
// Replace with your application-specific access checks.
}
export type
type Claims = {
sub?: string;
}
Claims
= {
sub?: string
sub
?: string
}
  • validatePayload runs once per connection and rejects mismatched tokens before LiveStore upgrades to WebSocket.
  • onPush (and onPull, if you need it) must repeat the verification because the payload forwarded to the Durable Object is the original client input.
  • All transports (WebSocket, HTTP, and DO-RPC) forward the payload to onPush/onPull, so this verification applies uniformly regardless of transport.

You can extend ensureAuthorized to project additional claims, memoise verification per authToken, or enforce application-specific policies without changing LiveStore internals.

If you prefer cookie-based authentication (e.g., with better-auth), you can forward HTTP headers to your onPush and onPull callbacks using the forwardHeaders option.

Passing tokens in URL parameters (syncPayload) exposes them in browser history, server logs, and referrer headers. Cookie-based auth avoids these issues since cookies are sent automatically with each request and aren’t logged in URLs.

The following example forwards Cookie and Authorization headers to the Durable Object callbacks:

  1. Configure forwardHeaders in makeDurableObject() to specify which headers to forward.
  2. Headers are stored in the WebSocket attachment during connection upgrade, surviving hibernation.
  3. Access headers via context.headers in onPush and onPull callbacks.
  4. Worker-level validation can also access headers via context.headers in validatePayload.

For more control, pass a function to forwardHeaders:

export class SyncBackendDO extends makeDurableObject({
forwardHeaders: (request) => ({
'x-user-id': request.headers.get('x-user-id') ?? '',
'x-session': request.headers.get('cookie')?.split('session=')[1]?.split(';')[0] ?? '',
}),
// ...
}) {}

LiveStore’s clientId identifies a client instance, while user identity is an application-level concern that must be modeled through your application’s events and logic.

  • clientId: Automatically managed by LiveStore, identifies a client instance
  • User identity: Managed by your application through events and syncPayload

The syncPayload is primarily intended for authentication purposes:

const
const useAppStore: () => Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi
useAppStore
= () =>
useStore<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>(options: RegistryStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>): Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi

Returns a store instance augmented with hooks (store.useQuery() and store.useClientDocument()) for reactive queries.

@example

function Issue() {
// Suspends until loaded or returns immediately if already loaded
const issueStore = useStore(issueStoreOptions('abc123'))
const [issue] = issueStore.useQuery(queryDb(tables.issue.select()))
const toggleStatus = () =>
issueStore.commit(
issueEvents.issueStatusChanged({
id: issue.id,
status: issue.status === 'done' ? 'todo' : 'done',
}),
)
const preloadParentIssue = (issueId: string) =>
storeRegistry.preload({
...issueStoreOptions(issueId),
unusedCacheTime: 10_000,
})
return (
<>
<h2>{issue.title}</h2>
<button onClick={() => toggleStatus()}>Toggle Status</button>
<button onMouseEnter={() => preloadParentIssue(issue.parentIssueId)}>Open Parent Issue</button>
</>
)
}

@returnsThe loaded store instance augmented with React hooks

@throwsunknown - store loading error or if called outside <StoreRegistryProvider>

useStore
({
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
,
CreateStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>.schema: LiveStoreSchema<DbSchema, EventDefRecord>

The LiveStore schema defining tables, events, and materializers.

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

Adapter used for data storage and synchronization.

adapter
,
CreateStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, Codec<Json, Json, never, never>>.batchUpdates?: (run: () => void) => void

Needed in React so LiveStore can apply multiple events in a single render.

@example

// With React DOM
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
// With React Native
import { unstable_batchedUpdates as batchUpdates } from 'react-native'

batchUpdates
,
CreateStoreOptions<LiveStoreSchema<DbSchema, EventDefRecord>, {}, 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
:
const user: {
jwt: string;
}
user
.
jwt: string
jwt
, // Using a JWT
},
})
export const
const App: () => JSX.Element
App
= () => {
const [
const storeRegistry: StoreRegistry
storeRegistry
] =
useState<StoreRegistry>(initialState: StoreRegistry | (() => StoreRegistry)): [StoreRegistry, Dispatch<SetStateAction<StoreRegistry>>] (+1 overload)

Returns a stateful value, and a function to update it.

@version16.8.0

@seehttps://react.dev/reference/react/useState

useState
(() => new
new StoreRegistry(config?: StoreRegistryConfig): StoreRegistry

Creates a new StoreRegistry instance.

@example

const registry = new StoreRegistry({
defaultOptions: {
batchUpdates,
unusedCacheTime: 30_000,
}
})

StoreRegistry
())
return (
<
const StoreRegistryProvider: ({ storeRegistry, children }: StoreRegistryProviderProps) => JSX.Element

React context provider that makes a

StoreRegistry

available to descendant components.

Wrap your application (or a subtree) with this provider to enable

useStore

and

useStoreRegistry

hooks within that tree.

@example

import { StoreRegistry } from '@livestore/livestore'
import { StoreRegistryProvider } from '@livestore/react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
const storeRegistry = new StoreRegistry({
defaultOptions: { batchUpdates }
})
function App() {
return (
<StoreRegistryProvider storeRegistry={storeRegistry}>
<MyComponent />
</StoreRegistryProvider>
)
}

StoreRegistryProvider
storeRegistry: StoreRegistry
storeRegistry
={
const storeRegistry: StoreRegistry
storeRegistry
}>
<
const AppContent: () => JSX.Element
AppContent
/>
</
const StoreRegistryProvider: ({ storeRegistry, children }: StoreRegistryProviderProps) => JSX.Element

React context provider that makes a

StoreRegistry

available to descendant components.

Wrap your application (or a subtree) with this provider to enable

useStore

and

useStoreRegistry

hooks within that tree.

@example

import { StoreRegistry } from '@livestore/livestore'
import { StoreRegistryProvider } from '@livestore/react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
const storeRegistry = new StoreRegistry({
defaultOptions: { batchUpdates }
})
function App() {
return (
<StoreRegistryProvider storeRegistry={storeRegistry}>
<MyComponent />
</StoreRegistryProvider>
)
}

StoreRegistryProvider
>
</
const Suspense: ExoticComponent<SuspenseProps>

Lets you display a fallback until its children have finished loading.

@seehttps://react.dev/reference/react/Suspense React Docs

@example

import { Suspense } from 'react';
<Suspense fallback={<Loading />}>
<ProfileDetails />
</Suspense>

Suspense
>
)
}
const
const AppContent: () => JSX.Element
AppContent
= () => {
const
const _store: Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi
_store
=
const useAppStore: () => Store<LiveStoreSchema<DbSchema, EventDefRecord>, {}> & ReactApi
useAppStore
()
// Use the store in your components
return <
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{/* Your app content */}</
JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
}

User identification and semantic data (like user IDs) should typically be handled through your event payloads and application state rather than relying solely on the sync payload.