Auth
LiveStore doesn’t include built-in authentication or authorization support, but you can implement it in your app’s logic.
Pass an auth payload to the sync backend
Section titled “Pass an auth payload to the sync backend”Use the syncPayload store option to send a custom payload to your sync backend.
Example
Section titled “Example”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.
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.
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.
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.
useState(() => new new StoreRegistry(config?: StoreRegistryConfig): StoreRegistry
Creates a new StoreRegistry instance.
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.
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.
StoreRegistryProvider> </const Suspense: ExoticComponent<SuspenseProps>
Lets you display a fallback until its children have finished loading.
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 fileimport { 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 fileimport { 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.
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.
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.
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: ErrorConstructornew (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: ErrorConstructornew (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.
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.
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: ErrorConstructornew (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
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'.
jwtVerify(token: string
token, new var TextEncoder: new () => TextEncoder
The TextEncoder interface enables you to encode a JavaScript string using UTF-8.
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.
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.
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.
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: ErrorConstructornew (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: ErrorConstructornew (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: ErrorConstructornew (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: ErrorConstructornew (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.
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 fileimport { 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}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: ErrorConstructornew (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;}
SyncPayloadvalidatePayloadruns once per connection and rejects mismatched tokens before LiveStore upgrades to WebSocket.onPush(andonPull, 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.
Cookie-based authentication
Section titled “Cookie-based authentication”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.
Why forward headers?
Section titled “Why forward headers?”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.
Example
Section titled “Example”The following example forwards Cookie and Authorization headers to the Durable Object callbacks:
How it works
Section titled “How it works”- Configure
forwardHeadersinmakeDurableObject()to specify which headers to forward. - Headers are stored in the WebSocket attachment during connection upgrade, surviving hibernation.
- Access headers via
context.headersinonPushandonPullcallbacks. - Worker-level validation can also access headers via
context.headersinvalidatePayload.
Custom header extraction
Section titled “Custom header extraction”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] ?? '', }), // ...}) {}Client identity vs user identity
Section titled “Client identity vs user identity”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.
Key points
Section titled “Key points”clientId: Automatically managed by LiveStore, identifies a client instance- User identity: Managed by your application through events and syncPayload
Using syncPayload for authentication
Section titled “Using syncPayload for authentication”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.
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.
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.
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.
useState(() => new new StoreRegistry(config?: StoreRegistryConfig): StoreRegistry
Creates a new StoreRegistry instance.
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.
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.
StoreRegistryProvider> </const Suspense: ExoticComponent<SuspenseProps>
Lets you display a fallback until its children have finished loading.
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.