fix(saas): collapse duplicate Supabase client to one GoTrueClient

The console warned "Multiple GoTrueClient instances detected in the same
browser context" and storage endpoints (/api/v1/storage/folders,
/files) kept 401ing even after a successful token refresh.

Cause: the SaaS bundle instantiated TWO Supabase clients on the same
sb-<ref>-auth-token storage key. :saas/auth/supabase.ts creates the
primary client (used by UseSession + apiClient), while billing /
licensing / user-management code imports @app/services/supabaseClient,
which fell through to :proprietary/services/supabaseClient.ts and called
createClient() again. Each client runs its own autoRefreshToken timer,
so they rotate the refresh token out from under each other → "Already
Used" refresh failures and spurious 401s, plus a residual /login flash.

Add a :saas override of @app/services/supabaseClient that re-exports the
single instance from @app/auth/supabase. The path mapping
(@app/* → src/saas/* → src/proprietary/* → src/core/*) now resolves
every consumer to the same client, so the :proprietary createClient() is
never bundled in the SaaS build.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Anthony Stirling
2026-06-10 11:26:27 +01:00
co-authored by Claude Opus 4.8
parent 1135bd9b63
commit 06476ea69e
@@ -0,0 +1,22 @@
// SaaS-layer override for `@app/services/supabaseClient`.
//
// There must be exactly ONE Supabase client (one GoTrueClient) per browser
// context. The :proprietary version of this module calls createClient() a
// second time with the same auth-token storage key as :saas's
// `@app/auth/supabase`. In the SaaS bundle, billing/licensing/user-management
// code (CheckoutContext, licenseService, AdminPlanSection, userManagementService)
// imports `@app/services/supabaseClient` and would otherwise pull in that second
// client. Two clients => two independent autoRefreshToken timers racing on the
// same refresh token => "Multiple GoTrueClient instances detected", rotated /
// "Already Used" refresh tokens, and spurious 401s (e.g. /api/v1/storage/*).
//
// Re-exporting the singleton from `@app/auth/supabase` keeps every
// `@app/services/supabaseClient` consumer pointed at the same instance, so the
// :proprietary module's createClient() is never bundled in the SaaS build.
import type { SupabaseClient } from "@supabase/supabase-js";
import { supabase as supabaseSingleton } from "@app/auth/supabase";
// `@app/auth/supabase` throws on missing config, so in the SaaS build the
// client is always present and Supabase is always configured.
export const supabase: SupabaseClient | null = supabaseSingleton;
export const isSupabaseConfigured = true;