Core Concepts

Offline Write Queue

What happens to a write when the network isn't there to receive it — and the one category of write that should never go through this system at all.

The core idea: a write is never lost, only delayed

When a channel is registered with a queueConfig, any write to it that can't reach the server immediately — because the device is offline, or the request simply failed — is captured and held rather than discarded. The moment connectivity returns, it's sent automatically. Your UI code for "save this note" doesn't need an offline branch; the same call works identically online or off.

1

Turning a channel into an offline-safe write path

Add a queueConfig to any registration and every write against that channel becomes offline-safe. Nothing else about the registration changes.
lib/flux.ts (excerpt)
1flux.register({
2 channel: 'notes',
3 idbKey: 'notes:mine',
4 ttl: 'medium',
5
6 ingestionType: 'COLLECTION_ALL',
7 diffBeforeUpdate: true,
8 event: 'UPDATE',
9 store: notesStoreAdapter,
10
11 // Turns writes to this channel into an offline-safe queue. A note
12 // saved while offline is captured here and sent automatically the
13 // moment connectivity returns — no special handling in your UI code.
14 queueConfig: {
15 storeName: 'notes_mutation_queue',
16 replayEndpoint: '/api/notes',
17 method: 'POST',
18 },
19})
replayEndpoint is where the queued write gets sent once it's safe to send. Until then, it sits captured locally — surviving a page refresh, a browser crash, and even a tab freeze, so nothing is lost between the moment the user hits save and the moment the network is actually available to hear about it.
2

Duplicate writes are handled for you

A user submitting the same form twice — a double tap on a slow connection, a retried request after a timeout — is a normal offline scenario, not an edge case. dedupeField and dedupeStrategy let you decide what happens when that occurs while an earlier write for the same identifier is still queued.
lib/flux.ts (excerpt)
1flux.register({
2 channel: 'waitlist_signups',
3 idbKey: 'waitlist_signups:latest',
4 ttl: 'long',
5
6 ingestionType: 'COLLECTION_ALL',
7 event: 'UPDATE',
8 store: waitlistStoreAdapter,
9
10 queueConfig: {
11 storeName: 'waitlist_queue',
12 replayEndpoint: '/api/waitlist',
13 method: 'POST',
14
15 // If the same email is submitted twice while offline (a double
16 // tap, a retried form), the second write merges into the first
17 // instead of creating a duplicate queued entry.
18 dedupeField: 'email',
19 dedupeStrategy: 'replace',
20 },
21})
'reject' blocks the second write outright — the first one queued wins. 'replace' merges the new payload into the still-queued entry, so the most recent input wins without producing two separate requests once the connection returns.
3

Storage is resilient across three tiers

Queued writes don't rely on a single storage mechanism succeeding. If the primary layer isn't available — some private-browsing modes restrict it — Flux automatically falls back to the next most durable option it has, rather than losing the write outright. You can observe this happening if you want to reflect it in your UI:
lib/flux.ts (excerpt)
1const flux = createFlux({
2 adapter: createSupabaseAdapter(supabase),
3
4 // Fires if IndexedDB is unavailable (e.g. Safari private browsing)
5 // and Flux has fallen back to a different storage layer for queued
6 // writes. Worth surfacing to the user in that specific case — data
7 // is still safe, just less durable across a hard refresh.
8 onStorageFallback: (layer, reason) => {
9 console.warn('[flux] Offline writes now backed by', layer, '—', reason)
10 },
11})
In practice this callback rarely fires — it exists for the narrow set of restrictive browser environments where the primary storage layer genuinely isn't usable, not as something every app needs to handle.
4

Multiple open tabs never duplicate a write

If a user has your app open in two tabs and queues a write in one of them, the other tab is made aware of that queued entry without independently writing its own copy to storage. This means the queue's contents — and its count, if you're displaying one — stay consistent across every open tab, and reconnecting in any one tab doesn't risk sending the same write twice because two tabs both thought they owned it.
What should never go through this queue

The offline write queue is built for writes where "send it a little later" is an acceptable outcome — a saved draft, a preference change, a beneficiary added to an account. It is deliberately not built, and should never be used, for financial ledger writes: payments, wire transfers, direct debits, or any operation where a duplicate send or a delayed send against changed conditions has real financial consequences.

There is no configuration on the queue that makes this safe. A queued payment that replays after reconnecting could execute against a balance, a rate, or an authorization that's no longer valid the moment it was queued under. The correct pattern is to keep payment endpoints entirely outside the queue system and instead block the action at the UI level while the app is offline — failing fast and visibly, rather than deferring silently.

Register the channel with no queueConfig at all:
lib/flux.ts (excerpt)
1// Payment and other financial-ledger endpoints should NEVER be
2// registered with a queueConfig. There is no such thing as a "safe"
3// way to queue a wire transfer or a card charge for later — the risk
4// of double-charging or charging against a state that's since changed
5// is not one Flux (or any offline queue) should be making on your
6// behalf.
7//
8// Instead, register the channel with no queueConfig at all, and gate
9// the write path itself on connectivity.
10flux.register({
11 channel: 'billing_account',
12 idbKey: 'billing_account:mine',
13 ttl: 'short',
14 ingestionType: 'SNAPSHOT',
15 diffBeforeUpdate: true,
16 event: 'UPDATE',
17 store: billingStoreAdapter,
18 // No queueConfig — a failed or offline payment attempt is never
19 // silently retried later. It fails immediately and visibly.
20})
Then gate the write action itself on live connectivity, using isOnline from useFlux() — the same flag every other part of your app can read to know whether the device currently has a connection:
components/CheckoutButton.tsx
1// components/CheckoutButton.tsx
2import { useFlux } from '@tsworldtech/flux-react'
3
4export function CheckoutButton() {
5 // isOnline reflects live connectivity — this is the flag to gate
6 // any financial write on, not a queue result you'd check later.
7 const { isOnline } = useFlux()
8
9 if (!isOnline) {
10 return (
11 <div role="status">
12 <p>You're offline right now.</p>
13 <p>Payments can't be made without a connection. Please reconnect to check out.</p>
14 <button disabled>Pay now</button>
15 </div>
16 )
17 }
18
19 return <button onClick={submitPayment}>Pay now</button>
20}
This pattern applies to any endpoint where the cost of a delayed or duplicated write outweighs the convenience of it happening automatically later — payment submission is the clearest example, but the same reasoning applies to any one-shot, consequential action your app exposes.

See core-concepts/conflict-resolution for what happens when a queued write for a non-financial record conflicts with a change made on the server while the user was offline, and core-concepts/mutation-pipeline for shaping rapid-fire writes — like autosave — before they ever reach the queue.