API Reference

register()

Registers one store against Flux — IDB caching, realtime updates, the offline write queue, and job tracking all attach to a store through this one call. Everything else in Flux is built on top of what you configure here.

Prerequisites
  • flux created via createFlux() — see api/create-flux
  • A state manager store — Zustand natively, or a Redux/Jotai adapter
1

Basic usage

Call flux.register() once per store, typically at module scope alongside the store definition itself — not inside a component body. It returns a cleanup function.
typescript
1const unregister = flux.register({
2 store: useProductsStore,
3 channel: 'products',
4 event: 'UPDATE',
5 idbKey: 'products:all',
6 ttl: 'medium',
7 ingestionType: 'COLLECTION_ALL',
8 hydrateState: (store, data) => store.getState().setProducts(data),
9})
10
11// register() returns a cleanup function — call it to tear the
12// registration down (unsubscribe realtime, stop any poller, drop the lock entry)
13unregister()
Where to call this

Register stores at import time, next to where the store itself is defined. If a registration needs to come and go with a component's lifetime (rare — most stores live for the whole session), call register() in a useEffect and return the cleanup function it gives you.

2

Config reference

Only store, channel, event, idbKey, and ttl are required. Everything else is opt-in per store — a store with none of the optional fields set still gets full IDB caching and realtime sync.
typescript
1interface StoreRegistration<T = any> {
2 store: any // your Zustand/Redux/Jotai store (or adapter)
3 channel: string // realtime channel name + bootstrap map key
4 table?: string // actual DB table, if it differs from channel
5 event: string // realtime event name, e.g. 'UPDATE' or '*'
6 idbKey: string // IndexedDB key this store is cached under
7 ttl: 'short' | 'medium' | 'long' // which of your configured TTLs applies
8 scope?: 'global' | 'user' // isolation boundary — see step 5 below
9 ingestionType?: IngestionType // 'SNAPSHOT' | 'COLLECTION_ALL' | 'LRU' | 'PAGINATED'
10 cacheStrategy?: CacheStrategyConfig
11 diffBeforeUpdate?: boolean // skip dispatch if incoming data === cached data
12 optimistic?: OptimisticConfig
13 hydrateState?: (store: any, data: T) => void // omit to use store.setState(data)
14 swCacheRoutes?: (data: T) => string[]
15 onStale?: () => void
16 queueConfig?: QueueConfig // enables the offline write queue for this store
17 pipeline?: MutationPipelineConfig
18 jobTracker?: JobTrackerConfig
19 onRealtimeUpdate?: (store: any, event: FluxNormalizedEvent) => void
20}
3

ingestionType

Tells Flux the shape of the data this channel carries, which changes how it's cached and hydrated. Defaults to COLLECTION_ALL if omitted.
typescript
1// SNAPSHOT — a single object: homepage, siteConfig, termsPage
2flux.register({ channel: 'homepage', ingestionType: 'SNAPSHOT', /* ... */ })
3
4// COLLECTION_ALL — a bounded array fetched entirely: services, team, pricing
5flux.register({ channel: 'services', ingestionType: 'COLLECTION_ALL', /* ... */ })
6
7// LRU — detail pages capped at a byte budget: products, articles
8flux.register({ channel: 'blog_post_detail', ingestionType: 'LRU', cacheStrategy: { type: 'LRU', maxBytes: 8_000_000 }, /* ... */ })
Full behavior for each type — including why LRU stores are read on-demand rather than hydrated at boot — is covered per-type under ingestion/*.
4

cacheStrategy

Only relevant when ingestionType is LRU. Sets the byte budget for the partition — set maxBytes directly rather than maxEntries, which is only a rough backwards-compatible estimate.
typescript
1interface CacheStrategyConfig {
2 type: 'NONE' | 'LRU'
3 maxBytes?: number // primary field to set — the partition's byte budget
4 maxEntries?: number // legacy fallback, converted to an estimated byte budget
5 trackAccessTime?: boolean
6}
7
8flux.register({
9 channel: 'blog_post_detail',
10 ingestionType: 'LRU',
11 cacheStrategy: { type: 'LRU', maxBytes: 8_000_000 }, // 8MB
12 /* ... */
13})
50MB hard ceiling

Whatever you configure, Flux clips the effective budget to 50MB and logs a console warning if clipping occurs — this protects against iOS Safari's aggressive storage eviction once an origin's usage spikes.

5

scope

Defaults to 'global' — this store's data survives logout and session timeout indefinitely. Set it to 'user' for anything tenant-specific: dashboards, billing, private queues.
typescript
1// Public data — survives logout indefinitely, this is the default
2flux.register({ channel: 'pricing', idbKey: 'pricing:latest', ttl: 'long', /* ... */ })
3
4// Per-user data — namespaced per tenant, purged on flux.clearUserSession()
5flux.register({
6 channel: 'dashboard',
7 idbKey: 'dashboard:summary',
8 ttl: 'short',
9 scope: 'user',
10 /* ... */
11})
Development-only naming warning

If a channel or idbKey looks sensitive — matching something like dashboard, billing, or account — but scope is left unset, Flux logs a console warning in development only. It's advisory, never blocking: unscoped means it won't be cleared on logout. See core-concepts/multi-tenant-scoping.

6

queueConfig — offline writes

Adding queueConfig turns this store's mutations into offline-safe writes: they commit to IndexedDB immediately and replay automatically once the connection returns.
typescript
1flux.register({
2 channel: 'profile',
3 scope: 'user',
4 queueConfig: {
5 storeName: 'profile',
6 replayEndpoint: '/api/profile',
7 method: 'PATCH',
8 replayStrategy: 'handshake', // 'fire_and_forget' | 'handshake'
9 dedupeField: 'userId',
10 dedupeStrategy: 'replace',
11 },
12 /* ... */
13})
The full contract — replayStrategy, revalidateFn, and how conflicts surface — is covered in core-concepts/offline-queue and core-concepts/conflict-resolution.
7

jobTracker — async job progress

Adding jobTracker interprets realtime row updates on this channel as job status transitions and fires progress/completion callbacks automatically.
typescript
1flux.register({
2 channel: 'video_jobs',
3 table: 'video_processing_jobs', // realtime table, if it differs from channel
4 jobTracker: {
5 trackBy: 'job_id',
6 onProgress: (store, percent) => store.getState().setProgress(percent),
7 onComplete: (store, payload) => store.getState().setResult(payload),
8 onFailure: (store, error) => store.getState().setError(error),
9 },
10 /* ... */
11})
For jobs that don't exist yet at registration time — e.g. created by an offline-queued upload once it replays — use flux.trackJob() instead. See api/track-job.
8

Callbacks

hydrateState, onStale, and onRealtimeUpdate give you hooks into the three moments data can reach this store — cold boot from IDB, a bootstrap fetch, and a live realtime event.
typescript
1flux.register({
2 channel: 'homepage',
3 idbKey: 'homepage:latest',
4 ttl: 'long',
5
6 // Called for every dispatch — IDB cold boot, bootstrap fetch, and every
7 // realtime event. Omit this and Flux calls store.setState(data) instead.
8 hydrateState: (store, data) => store.getState().setHomepage(data),
9
10 // Fired when hydration serves data from IDB but the TTL has already expired —
11 // the UI shows something instantly while a fresh fetch is still pending.
12 onStale: () => console.log('homepage served stale, refetch pending'),
13
14 // Fired on every realtime event, in addition to the normal dispatch —
15 // useful for side effects (toasts, analytics) that shouldn't live in hydrateState.
16 onRealtimeUpdate: (store, event) => {
17 if (event.op === 'DELETE') toast('A page was removed')
18 },
19})
9

Unregistering and hot reload

Calling the function register() returns tears down the realtime subscription and any job poller for that channel. In development, re-registering an already-registered channel silently replaces it — safe for HMR. In production, a duplicate registration logs a warning and is skipped rather than silently replacing a live store.

For populating every registered store in one network call, see api/bootstrap. For the isolation model behind scope, see core-concepts/multi-tenant-scoping.