Core Concepts

The 5-Layer Resilience Stack

Flux turns the browser into a resilient data layer using five cooperating layers — a local cache, an offline write queue, a cross-tab-safe replay engine, a realtime sync layer, and a single intake gate that keeps all of it consistent. This page explains what each layer does and why there are five of them instead of one big cache.

Why five layers instead of one cache

A cache alone answers one question: "do I have this data already?" It has nothing to say about what happens when the user is offline and tries to change that data, when two browser tabs are open at once, when the network comes back after ten minutes away, or when a live update arrives at the exact moment a page is loading from disk. Each of those is a different failure mode, and bolting all of them onto a single cache layer is how caching libraries turn into unmaintainable piles of edge-case flags.

Flux instead gives each failure mode its own layer, with a narrow job and a clear boundary to the layer next to it. You never have to think about which layer is doing what day to day — register() activates the right ones for you — but understanding the five makes Flux's behavior predictable instead of magic.

1

Layer 1 — Local Cache

The foundation. Every registered channel's data lives in IndexedDB behind a TTL — short, medium, or long, your choice per channel — so a returning visitor's app paints from disk before any network request has even been sent. Data past its TTL isn't discarded; it's marked stale and served immediately anyway, while a fresh copy is fetched behind the scenes. Your UI never shows a blank loading state for data it has already seen once.
For unbounded collections — a library of articles, a catalog of products — this layer applies a byte-budget eviction policy rather than a fixed item count, so cache size stays predictable regardless of how large individual entries are. The budget is configurable per channel and is automatically kept within a sane ceiling so a misconfigured value can't fill up a visitor's device storage.
2

Layer 2 — Offline Write Queue

When a user submits a change while offline, or while a request fails, that mutation doesn't vanish — it's captured and queued for automatic replay the moment connectivity returns. The queue itself is resilient across three separate storage tiers, so it survives everything from a normal page refresh to restrictive private-browsing environments. If the primary storage layer is ever unavailable, Flux degrades gracefully to the next one and tells you it did, rather than silently losing writes.
Duplicate submissions are handled at this layer too — you can configure a channel to reject a second write for the same identifier while one is already queued, or to merge the two into one, so rapid-fire edits (an autosave, a double-tapped button) don't produce a pile of redundant requests once the connection comes back.
3

Layer 3 — Coordinated Replay

Replaying a queue safely across multiple open browser tabs is its own problem — without coordination, two tabs could both try to replay the same queued mutation at once, or one tab's replay could race against a check the other tab just ran. This layer elects a single tab to own replay for a given queue at any moment, so exactly one execution happens regardless of how many tabs are open.
For writes that update an existing record rather than creating a new one, this layer also supports a conflict-aware replay mode: before resending a queued change, it checks whether the server's copy of that record changed while the user was offline, and gives you a single hook to resolve any conflicts found — rather than blindly overwriting whatever the server has. See core-concepts/conflict-resolution for the full model.
4

Layer 4 — Realtime Sync

Live updates are delivered through a database-agnostic adapter interface, so the same registration code works whether your backend speaks Supabase, Socket.io, SSE, or raw WebSockets — and a polling adapter is built in for backends with no realtime transport at all. Whichever adapter you use, only one browser tab ever holds the live connection at a time; every other open tab receives updates relayed from that one tab instead of opening a redundant connection of its own. Your server sees one connection per device, not one per tab.
After a period offline, this layer is also what lets a reconnecting client catch up on exactly what it missed, rather than re-fetching everything from scratch — see core-concepts/sync-anchors and core-concepts/activity-bus for how that catch-up is coordinated against your regular cache-freshness checks so the two never fire redundant requests against each other.
5

Layer 5 — The Unified Intake Gate

Data can enter your app's state from three different places — a cold read from the local cache, a realtime event, or a network fetch — and all three funnel through one shared gate before touching your state manager. This is what guarantees that no matter which path data came from, the same rules apply every time: your state manager gets updated, the local cache gets refreshed, other open tabs get notified, and any images or assets in the payload get warmed — consistently, in the same order, every time.
Practically, this is why a store you register with Flux never needs its own special-case logic for "this update came from the network" versus "this update came from cache" — from the store's point of view, there's exactly one way data ever arrives.
You don't configure these layers directly — you configure a channel, and Flux activates whichever layers that configuration needs:
lib/flux.ts (excerpt)
1// A single register() call is what activates all five layers
2// for one piece of data — nothing else to wire up.
3flux.register({
4 channel: 'dashboard_metrics',
5 idbKey: 'dashboard_metrics:latest',
6 ttl: 'long',
7 ingestionType: 'COLLECTION_ALL',
8 diffBeforeUpdate: true,
9 event: 'UPDATE',
10 store: dashboardStoreAdapter,
11
12 // Layer 2 — turns this channel's writes into an offline-safe queue
13 queueConfig: {
14 storeName: 'dashboard_mutation_queue',
15 replayEndpoint: '/api/dashboard',
16 method: 'POST',
17 },
18})
Layers activate per channel, not globally

Every layer is optional in the sense that a channel only pays for what it uses — a read-only public dataset only exercises Layers 1, 4, and 5. A channel with a queueConfig also gets Layers 2 and 3. You never choose layers directly; they activate based on what you configure on register().

Each layer is documented in more depth on its own page — see core-concepts/idb-ttl (Layer 1), core-concepts/offline-queue (Layer 2), core-concepts/conflict-resolution (Layer 3), core-concepts/sync-anchors and core-concepts/activity-bus (Layer 4), and core-concepts/multi-tenant-scoping, which cuts across all five layers to keep per-user data isolated and cleanly purged on logout.