API Reference

bootstrap()

Replaces every individual store's fetch call with one network request per scope. Leader election, TTL-aware revalidation, dynamic route chunk warming, and clock skew correction all attach to this one call — everything downstream just reads what it dispatches.

Prerequisites
  • flux created via createFlux() — see api/create-flux
  • Stores already registered via register() for every key in your map
1

Basic usage

Call flux.bootstrap() once for public data on mount, and again with scope: 'user' once auth resolves. The two calls run under separate leader locks and cooldown timers, so one never blocks or resets the other.
typescript
1// Public data — fires on mount, before auth resolves
2await flux.bootstrap({
3 endpoint: '/api/bootstrap',
4 map: {
5 homepage: 'homepage',
6 services: 'services',
7 pricing: 'pricing',
8 },
9})
10
11// Per-user data — fires again once auth resolves, scoped independently
12await flux.bootstrap({
13 endpoint: '/api/bootstrap/user',
14 map: { dashboard: 'dashboard' },
15 scope: 'user',
16 userId: session.user.id,
17})
One call per scope, not per store

map can carry every channel you've registered for that scope. Whichever tab wins the leader lock fetches all of it in a single network request and dispatches each key to its matching store.

2

Config reference

Only endpoint and map are required. scope defaults to 'global'.
typescript
1interface BootstrapConfig {
2 endpoint: string // your single-packet bootstrap route
3 map: Record<string, any> // response data key -> registered channel
4 cooldownMs?: number // default 30s, tracked independently per scope
5 scope?: StorageScope // 'global' | 'user' — defaults to 'global'
6 userId?: string | null // required when scope is 'user'
7 authResolved?: boolean // only meaningful when scope is 'global' — see step 4
8}
3

scope and userId

A 'user'-scope call requires userId — Flux throws otherwise rather than silently writing to the wrong namespace. It's also what drives the boot-time stale-namespace sweep: if the incoming userId differs from the last one recorded on this browser, the previous user's entire IDB cache is evicted first.
typescript
1// 'global' — public data, no userId needed
2await flux.bootstrap({
3 endpoint: '/api/bootstrap',
4 map: { pricing: 'pricing' },
5})
6
7// 'user' — requires userId, throws otherwise
8await flux.bootstrap({
9 endpoint: '/api/bootstrap/user',
10 map: { dashboard: 'dashboard' },
11 scope: 'user',
12 userId: currentUser.id, // required
13})
Why this matters on shared machines

This sweep is what catches a session that never called clearUserSession() at all — a crashed tab, a closed laptop lid, a silently expired cookie. See core-concepts/multi-tenant-scoping.

4

authResolved — the anonymous sweep gate

Only meaningful on a 'global'-scope call. It tells Flux you've definitively confirmed there's no active session right now — not merely that the user bootstrap hasn't run yet. Without this gate, the normal "public bootstrap on mount, user bootstrap once auth resolves" ordering would look identical to a logout and wipe a still-valid session's cache on every page load.
typescript
1// Public bootstrap on mount — do NOT set authResolved here.
2// currentUserId is null at this instant simply because the
3// user-scope bootstrap hasn't fired yet, not because of a logout.
4await flux.bootstrap({
5 endpoint: '/api/bootstrap',
6 map: { pricing: 'pricing' },
7})
8
9// Only after your own auth check has genuinely confirmed
10// there is no session do you pass authResolved: true
11const session = await getSession()
12if (!session) {
13 await flux.bootstrap({
14 endpoint: '/api/bootstrap',
15 map: { pricing: 'pricing' },
16 authResolved: true, // now safe to sweep the last user's cache
17 })
18}
5

Leader election and cooldown

Each scope gets its own bootstrap leader lock (flux:bootstrap:lead:global, flux:bootstrap:lead:user) and its own 30-second cooldown. One tab wins, fetches, and writes to IDB; every other open tab hydrates from disk instead of firing a second request.
typescript
1// Two tabs mount within the same second
2// Tab A — acquires flux:bootstrap:lead:global, fetches, writes IDB, releases
3// Tab B — acquires the same lock next, checks IDB TTL — fresh — hydrates
4// from disk only, no second network call
5
6// A concurrent 'user' bootstrap uses its own lock and its own
7// cooldown timer, so it never waits on or resets the 'global' one
8await flux.bootstrap({ endpoint: '/api/bootstrap', map: { pricing: 'pricing' } })
9await flux.bootstrap({
10 endpoint: '/api/bootstrap/user',
11 map: { dashboard: 'dashboard' },
12 scope: 'user',
13 userId: session.user.id,
14})
6

Response shape and asset warming

data keys are matched against map and dispatched to each registered store. assets — dynamic route chunk paths extracted server-side from your build manifest — get posted to the service worker as FLUX_WARM_CHUNKS so those routes work offline after this one call.
typescript
1// What your endpoint returns
2interface BootstrapResponse {
3 ok: boolean
4 data: Record<string, any> // keyed exactly like your map
5 assets: string[] // dynamic route chunks to warm in the SW
6}
7
8// map keys are matched against response.data keys, then
9// dispatched to whichever store was registered under that channel
10{
11 ok: true,
12 data: {
13 homepage: { title: '...', hero: '...' },
14 services: [ /* ... */ ],
15 },
16 assets: ['/_next/static/chunks/blog-[slug]-abc123.js'],
17}
7

Revalidation on focus and visibility

On window focus or a visibility change to visible, Flux checks IDB TTL for both scopes independently against each one's own blueprint, and only refetches the channels that are actually stale — never combining scopes into one refetch, since that would mis-stamp one scope's stores with the other's generation number.
typescript
1// Nothing to call directly — this runs automatically on window
2// focus and visibilitychange, once per scope, against that
3// scope's own blueprint written by the last successful bootstrap() call
4
5// localStorage key shape (one per scope):
6// flux_boot_blueprint:{storagePrefix}:global
7// flux_boot_blueprint:{storagePrefix}:user
Held open by realtime when it's present

If a realtime adapter is connected, this revalidation loop won't fire a redundant HTTP refetch while a delta stream is actively catching a stale channel up — see core-concepts/activity-bus.

8

Clock skew correction

Every successful bootstrap computes clockSkewMs from the response's Date header, correcting for latency. It's used internally for handshake replay timestamps, and exposed directly for anything your UI needs to display accurately.
typescript
1// Available on the engine after the first successful bootstrap()
2console.log(flux.clockSkewMs)
3
4// Or via the hook in flux-next / flux-react
5const now = useFluxTime() // Date.now() + flux.clockSkewMs
9

Error handling

A 5xx response resets that scope's cooldown to zero so the next online event retries immediately. Any other failure just serves whatever's already in IDB — bootstrap never rolls back a store to empty.
typescript
1try {
2 await flux.bootstrap({ endpoint: '/api/bootstrap', map: { pricing: 'pricing' } })
3} catch {
4 // On 5xx, Flux already reset this scope's cooldown to 0 internally —
5 // the next online event retries immediately, no action needed here.
6 // On any other failure, existing IDB data is served as-is — never rolled back.
7}

For what happens to a 'user'-scoped bootstrap on logout, see api/clear-user-session. For the isolation model behind scope, see core-concepts/multi-tenant-scoping. For registering the stores this call dispatches into, see api/register.