IDB Engine & TTL
How Flux decides what to keep on disk, how it knows whether that data is still current, and what happens the instant it is not. This is Layer 1 of the resilience stack — the reason a returning visitor's app paints instantly instead of waiting on the network.
Cached data is never deleted because time passed, and for a channel that realtime keeps current, time passing does not make it stale either. A channel is fresh when something has been watching it since it was last confirmed. A TTL is the fallback freshness policy for the channels nothing can watch. Expired data is still served instantly from disk; what happens next depends on whether this channel is covered, which is the part most caching systems get wrong. Read on.
Every registered channel gets a TTL
ttl is a required field on every register() call, and it only ever takes one of three values: 'short', 'medium', or 'long'. You define what those three mean, in milliseconds, once in your createFlux() config, and every registration just picks the bucket that fits.short from 5 minutes to 1, say — is a one line change in one place.short for things that change often and where staleness is visible (live metrics, a queue count). medium for moderate churn (a notifications list, a team roster). long for data that rarely changes (pricing tables, site configuration, terms pages). The bucket matters most for channels realtime cannot cover, covered next.Which channels realtime can cover
Delta. A realtime binding plus delta catch-up. Live events keep the channel current, and any gap (offline, a dropped socket) is closed by asking the backend what changed. A refetch is only ever the exception.
Realtime only. A realtime binding but no delta support on the backend. Live events keep it current, but a gap can only be closed by refetching the channel.
TTL only. Realtime cannot cover it: no adapter is configured, or the registration has no table on a backend where realtime subscribes by table. The TTL is the only freshness mechanism, and a refetch happens when it elapses.
On Supabase, realtime subscribes to a table. A registration without table is not silently subscribed to a table named after the channel. It is treated as TTL-only, and a debug message says so. If a channel you expected to be live keeps refetching when its TTL elapses, this is the first thing to check.
A covered channel is never stale because time passed
A cache that treats every elapsed TTL as stale would refetch data it already knew was correct. Flux treats "something has been watching this channel since it was confirmed" as its own form of freshness, separate from the TTL clock, so your backend is never asked a question it already answered.
What happens when you go offline and come back
Back online, realtime comes up. The leader asks the backend what changed since the channel was last confirmed. Rows that come back are applied. If the backend answers that nothing changed, the channel is current as of that answer and no refetch happens. A confirmed empty answer is a real answer, not a missing one.
The backend cannot answer. If the gap is older than the backend's changelog reaches, or the catch-up failed or could not be verified, Flux refetches that one channel. It never marks it current on a guess, and one failed channel never holds up the others.
Realtime is degraded. After repeated connection failures, Flux refetches the channels whose data predates the gap once, while realtime keeps retrying in the background. After that, only the TTL decides.
Realtime is blocked by a quota or credentials. The channel behaves as TTL-only while realtime retries on a slow backoff.
Realtime is still connecting. Flux waits for the next signal. There is no timer that gives up after a fixed number of seconds.
Data is fetched once — global and per user
bootstrap() hydrates from disk instead of refetching whenever the channels are covered, or a gap is one a catch-up can close, or realtime simply is not up yet.What happens on every cold read
Fresh. The data is covered, or within its TTL for a channel with no coverage. It is dispatched to your store immediately and nothing else happens.
Stale. The data exists but Flux cannot vouch for it. It is still dispatched immediately, so the UI is never empty. onStale fires for a channel with no realtime coverage whose TTL has elapsed, for a channel with no coverage record yet whose TTL has elapsed (the first run after upgrading), and for a channel whose last catch-up came back too old or failed. It does not fire while offline, because there is nothing to act on, and it does not fire just because a covered channel's TTL elapsed.
Missing. Nothing is cached yet. Your store's initial or loading state is left as is and the UI waits for the network, for the very first visit only.
Two data shapes, two caching strategies
trackAccessTime: true means an entry the user is actively reading gets its "last accessed" clock refreshed, so it is not evicted mid read even if it is technically the oldest entry by write time. Realtime and catch-up rows only refresh entries the user already has cached; they never create entries for items nobody opened.An LRU channel with no maxBytes falls back to a sane 10MB default. Whatever you configure — or whatever the default resolves to — is automatically capped at a hard ceiling, so a misconfigured budget can never balloon on a visitor's device. Mobile browsers, Safari especially, are noticeably less forgiving about large origin storage usage than desktop, so keeping LRU budgets modest is worth doing deliberately.
Every app's cache is isolated
storagePrefix once, in your engine config, and every IDB entry Flux writes for this app is namespaced under it. This is what lets two independent Flux powered surfaces — say, a public marketing site and an authenticated dashboard — share a browser origin without their caches ever colliding. Per-user data, and each user's coverage record, live under that user's own namespace inside the prefix, which is why logging out can remove exactly one user's data and nothing else.Coverage only advances after the disk write succeeds
onStorageFallback handler, and the channel is simply fetched again when needed. A cache that claims to be current while the disk disagrees is worse than one that admits it is not.Images are handled separately from data
A single object or bounded array (homepage content, pricing, a team list) → ingestionType: 'SNAPSHOT' or 'COLLECTION_ALL', no cacheStrategy needed.
An unbounded, growing collection (articles, products, user generated content) → ingestionType: 'LRU' with an explicit maxBytes.
Data that should never trigger a redundant refetch → give the registration a table and use a realtime adapter with delta support (see core_concepts/sync_anchors). The TTL then only governs what realtime cannot cover, and a covered channel is never refetched for age alone.
See core_concepts/sync_anchors for how coverage and reconnect catch-up decide what changed while you were away, core_concepts/activity_bus for how Flux tracks catch-up results and network and realtime health, core_concepts/data_ingestion_strategies for the full breakdown of all four ingestion types, and core_concepts/offline_queue for how writes — not just reads — are made resilient on top of this same local cache.