Skip to content

Supabase Client Becomes Irrecoverably Corrupted After Tab Suspension (PWA/Browser) #36046

@simplysparsh

Description

@simplysparsh

Supabase Client Becomes Irrecoverably Corrupted After Tab Suspension (PWA/Browser)

Summary

After a few minutes of browser tab suspension or mobile app backgrounding (especially in PWAs or on Safari/iOS), the Supabase client enters a corrupted internal state. Once this happens:

  • functions.invoke() hangs silently or never resolves
  • auth.getSession() returns null despite a valid session
  • auth.signOut() no longer completes
  • Realtime channels fail silently and don’t reconnect
  • The only reliable recovery is to fully recreate the Supabase client via createClient()

What Works Initially

  • Supabase client initializes correctly
  • Auth/session is valid
  • Realtime channels and function calls operate as expected

What Fails After Idle

After 2–5 minutes of backgrounding:

  • functions.invoke() stalls without resolving
  • auth.getSession() returns null, even though a session exists
  • Realtime .on('close') fires, but .on('open') never follows
  • realtime.accessTokenValue becomes out-of-sync with auth.getSession()
  • auth.getUser() may return a user, but API calls return 401s

Root Cause

Supabase client internally shares state across:

  • Auth session and token
  • Realtime WebSocket
  • Function call promise queue

When the browser suspends or backgrounds the tab/app:

  • The WebSocket may silently disconnect
  • Session token state becomes desynchronized
  • Promises issued via .functions.invoke() may freeze

Because the client is monolithic, this internal corruption affects all areas of usage (auth, functions, realtime).


Current Workaround

We’ve implemented a wrapper that:

  • Detects provable corruption based on 5 deterministic checks:
    1. Internal promise hang (functions.invoke() hangs, raw fetch() works)
    2. Session desync (auth.getSession() returns null while session exists)
    3. Realtime token mismatch (realtime.accessTokenValue is stale)
    4. WebSocket zombie state (.on('close') fires but never .on('open'))
    5. False-positive authentication (getUser() returns a user, but requests 401)
  • Replaces the client with a new one using createClient()
  • Rehydrates session
  • Re-subscribes to Realtime channels

Repro Steps

  1. Use a PWA or leave an app open for 2-3 min and move to a different tab
  2. Authenticate a user and open a Realtime channel
  3. Background the tab for 3–5 minutes (or switch apps on mobile)
  4. Resume the tab/app
  5. Attempt to call auth.getSession() or functions.invoke()
  6. Observe: client fails to complete call or responds incorrectly

Suggested Fixes

  • Add public API for health:
    • supabaseClient.isCorrupted()
    • supabaseClient.reinitialize()
  • Internally self-heal:
    • Auto-sync tokens between .auth and .realtime
    • Auto-reconnect WebSocket with fallback validation
    • Retry .functions.invoke() if internal execution stalls
  • Provide official guidance for client recreation

Impact

This issue affects:

  • PWAs and mobile apps (especially on Safari and iOS)
  • Long-lived browser sessions
  • Background-tab use cases
  • Any app requiring real-time + session persistence without full reload

Environment

  • Browser(s): Chrome, Safari, Mobile Safari
  • App type: [SPA / PWA / React / Svelte / etc.]

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions