How to integrate authentication, pass auth tokens to the sync backend, enforce authorization, and encrypt event payloads in LiveStore.
LiveStore does not include built-in authentication or authorization. This guide explains how to integrate your own auth system, pass credentials to the sync backend, control access per store, and encrypt event payloads before they leave the client.
Authentication is an application-level concern in LiveStore. The framework does not issue sessions, validate tokens, or manage user identities. Your application owns the full auth flow — login, token refresh, and session management — and LiveStore provides a hook (syncPayload) to pass the result of that flow to your sync backend.This separation keeps LiveStore focused on state management and sync while letting you use any auth library you prefer (Auth.js, Better Auth, Clerk, Supabase Auth, custom JWTs, and so on).
This example uses jose, a JavaScript JWT library that works across Node.js, Cloudflare Workers, Deno, Bun, and browsers.
If the token fails validation, LiveStore rejects the sync connection but the client app continues to work locally. Any events committed while unauthenticated are queued and sync as soon as the user re-authenticates.
If your auth system uses cookies (for example, with Better Auth), use the forwardHeaders option to pass session cookies to your sync backend callbacks.Passing tokens in URL parameters (syncPayload) exposes them in browser history, server logs, and referrer headers. Cookie-based auth avoids these issues because cookies are sent automatically and are not visible in URLs.
Headers are stored in the WebSocket attachment during connection upgrade and survive hibernation, so they remain available in onPush and onPull callbacks throughout the connection lifetime.
Validating in both the worker and the Durable Object
Treat syncPayload as untrusted input. Validate the token in validatePayload (which runs once per connection at the Worker level) and then repeat the same verification inside the Durable Object’s onPush before trusting per-push metadata.
validatePayload runs once per WebSocket connection. For per-push authorization (checking that a specific user can write specific events), add verification inside onPush as well. Do not rely solely on the connection-level check.
The HTTP transport does not forward payloads today. If you need per-request authorization with HTTP transport, embed the necessary context directly in the event payloads or switch to WebSocket/DO-RPC transport.
LiveStore’s clientId identifies a client instance and is automatically managed by the framework. User identity is a separate, application-level concept.
clientId
Automatically assigned per client instance. Useful for sync coordination but not for user-level authorization.
User identity
Managed by your application. Pass user identity through syncPayload for connection-level auth, and include user IDs in event payloads for per-event attribution.
User identification and semantic data (such as userId on an event) should live in event payloads and application state, not solely in the sync payload.
You can implement encryption today by applying a custom Effect Schema transformation to your event definitions. The transformation encrypts the payload before the event is written to the log and decrypts it when the event is read back.
import { Schema } from '@livestore/livestore'// Example: wrap a schema with symmetric encryptionconst encryptedString = Schema.transform( Schema.String, Schema.String, { decode: (cipher) => decrypt(cipher, KEY), encode: (plain) => encrypt(plain, KEY), })const events = { messageCreated: Events.synced({ name: 'v1.MessageCreated', schema: Schema.Struct({ id: Schema.String, body: encryptedString, // encrypted at rest and in transit }), }),}
Do not hardcode keys in your application bundle. Use environment variables or a key management service.
Per-user or per-workspace keys limit the blast radius of a compromised key. If one key is exposed, only that user’s or workspace’s data is at risk.
Key rotation requires re-encrypting existing events in the log. Plan for this if you need long-term key hygiene.
End-to-end encryption (where the server never sees plaintext) means the server cannot index, search, or inspect event payloads. Design your sync backend accordingly.