Core Concepts

Offline Auth Intent

Flux doesn't authenticate anyone — that judgment call stays in your own auth glue. This page covers the one decision every Supabase + Flux app has to get right: telling a real logout apart from a session check that simply couldn't be confirmed because the device is offline.

Prerequisites
  • @tsworldtech/flux_supabase installed
  • A Supabase client constructed with createBrowserClient() or createClient()
  • An auth store or equivalent that listens to supabase.auth.onAuthStateChange()
Picture a laptop put to sleep while signed in, then reopened later somewhere with no internet. Supabase's client tries to refresh the session, the refresh fails, and onAuthStateChange fires with session: null. Nothing about that event says whether the user actually signed out or whether the device just can't reach the network right now — but code that treats a null session as a confirmed logout will call flux.clearUserSession() anyway, wiping cached dashboard data that IndexedDB was holding correctly the entire time.
navigator.onLine can't tell you either

navigator.onLine only reflects whether a network interface is up, not whether it actually reaches the internet. A laptop just woken from sleep, or connected to a router with no working WAN, commonly reports navigator.onLine === true for several seconds — sometimes longer — while every real request still fails.

1

Wire the connectivity aware fetch

Pass createConnectivityAwareFetch() as the client's global.fetch. Every request the Supabase client makes now feeds a shared, evidence based connectivity tracker instead of the browser's unreliable flag — it flips to offline only on a real fetch failure or the browser's offline event, and back to online only once a real request actually succeeds. It also fast fails: once offline is known, further requests reject instantly instead of waiting on the OS's own network timeout, and the very first unproven request is bounded to about 2.5 seconds.
typescript
1import { createBrowserClient } from "@supabase/ssr"
2import { createConnectivityAwareFetch } from "@tsworldtech/flux_supabase"
3
4const supabase = createBrowserClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
7 {
8 auth: {
9 // Flux takes manual control of the refresh timer — see Step 3.
10 autoRefreshToken: false,
11 },
12 global: {
13 fetch: createConnectivityAwareFetch(),
14 },
15 }
16)
2

Resolve intent inside onAuthStateChange

resolveAuthIntent(event, session) is the single, shared answer to "is this a real logout, or an unverified null session?" It reads from the same tracker the fetch wrapper feeds, so it can never drift out of sync with what the rest of your app believes about connectivity — which is exactly what went wrong when this check was written independently in more than one place.
typescript
1import { resolveAuthIntent } from "@tsworldtech/flux_supabase"
2
3supabase.auth.onAuthStateChange((event, session) => {
4 const intent = resolveAuthIntent(event, session)
5
6 // 'confirmed_user' → a real session. Treat as logged in.
7 // 'confirmed_logout' → explicit SIGNED_OUT, or a null session while
8 // genuinely online. Safe to clear.
9 // 'unverified' → null session, but NOT explicit sign out, and
10 // NOT confirmed online. Do not clear anything —
11 // this could just be an offline token refresh
12 // that failed.
13 setSessionUnverified(intent === "unverified")
14 setUser(session?.user ?? null)
15})
3

Only clear the session on a confirmed logout

Whatever calls flux.clearUserSession() — a store initializer, an auth listener, wherever your app wires it — should check sessionUnverified (or call isReallyOnline() directly) before purging. When the session is unverified, fall back to the last known cached user ID instead. flux.bootstrap()'s own session continuity guard makes this safe — if the cached ID matches, it hydrates purely from disk with zero network calls, regardless of whether the device is actually online.
typescript
1function handleAuthChange(userId: string | null, sessionUnverified: boolean) {
2 if (!userId) {
3 if (sessionUnverified || !isReallyOnline()) {
4 // Don't purge — we can't confirm this is a real logout.
5 return
6 }
7 flux.clearUserSession()
8 return
9 }
10
11 flux.bootstrap({ scope: 'user', userId, endpoint: '/api/bootstrap/user', map: { /* ... */ } })
12}
4

Reading connectivity elsewhere

Anywhere else in your app that needs to know the current connectivity state — a loading gate, a banner, a retry condition — should read from this same tracker rather than navigator.onLine directly. A second, independent connectivity check is how this class of bug creeps back in.
typescript
1import { isReallyOnline, subscribeConnectivity } from "@tsworldtech/flux_supabase"
2
3// Read the current, evidence based belief about connectivity —
4// never navigator.onLine directly.
5if (!isReallyOnline()) {
6 // fall back to cached data
7}
8
9// React the moment real connectivity is confirmed (a request actually
10// succeeded), not when the browser's 'online' event merely fires.
11const unsubscribe = subscribeConnectivity((online) => {
12 if (online) {
13 // safe to retry whatever was deferred
14 }
15})
What this doesn't fix

You may still see AuthRetryableFetchError and one or more failed token?grant_type=refresh_token requests logged to the console right after a cold, offline boot. This comes from Supabase's own client library performing its one time internal session recovery check at startup — it runs regardless of connectivity and has no public config knob to disable it. createConnectivityAwareFetch() bounds how long that check takes to fail (a few seconds at most instead of whatever the OS's real network stack takes), but it can't remove the check or its console output entirely. This is expected, harmless, and unrelated to what the user sees — cached data still renders immediately from IDB.

For how a resolved user ID drives per tenant caching and cleanup, see core_concepts/multi_tenant_scoping. For general adapter setup, see adapters/supabase.

Powered by Flux