Scope
Every store is tied to a scope — a set of identifiers that uniquely describe the current session context:createScopeKey(scope) to produce a composite string key in the form "appId:env:userId:orgId". That key names the IndexedDB database for this scope. Changing any part of the scope — for example, switching userId after a logout/login — creates a completely separate IndexedDB scope. You never have to manually clear data when users switch.
If
environment is omitted, its segment defaults to "default". If userId is omitted it defaults to "anonymous", and if orgId is omitted it defaults to "none". For single-user apps without auth, omitting both is fine.Collections
A collection is a named set of records with configurable persistence and query behavior. You define collections withdefineCollection:
persist— set tofalseto keep records in memory only (useful for transient UI state).deriveKey— turns query arguments into a stable cache key. Records fetched for{ orgId: "org-1" }are stored independently from records for{ orgId: "org-2" }.redactBeforePersist— called before writing to IndexedDB. Return a sanitized copy to strip sensitive fields, ornullto skip persistence for that record entirely.staleMs— marks a collection state as stale after this many milliseconds, signalling that a fresh server fetch is needed.
SyncStore
TheSyncStore is the central object. You create it once with createSyncStore and share it across your component tree via SyncProvider. Its main responsibilities are:
Calling
store.hydrate() loads all persisted records from IndexedDB, runs any pending migrations, and sets status.hydrated = true. All hook subscriptions re-render at this point.
Outbox
The outbox is CL Sync’s durability mechanism. Every time you callstore.enqueueMutation(), the following happens in order:
- The mutation’s
reducerruns immediately, applying an optimistic local state change. - The mutation arguments are written to the outbox table in IndexedDB.
- The mutation’s
flushfunction is called to send the change to your server.
store.hydrate() restores pending outbox items. After registering your mutation definitions, call store.flushPendingMutations() to retry them.
SyncStatus
store.getStatus() (and the useSyncStatus() hook) returns a live snapshot of sync state:
hydrated to gate your UI — until it’s true, records are not yet available from IndexedDB.
Optimistic updates
When you define areducer on a mutation, it runs synchronously and immediately when enqueueMutation is called. Your UI updates before any network request is made. If the flush call fails, you implement rollback logic in onReject:
onReject. This keeps the library simple and predictable while giving you full flexibility over conflict resolution.
Persistence modes
By default, CL Sync writes all persistent collections to IndexedDB. You can opt out at two levels:
Memory mode is useful for server-side rendering environments (where IndexedDB isn’t available) and for unit tests where you don’t want filesystem side effects.