From 06476ea69e19cc180bcb2e28bd48f1b6bbc7d3f1 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+frooodle@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:26:27 +0100 Subject: [PATCH] fix(saas): collapse duplicate Supabase client to one GoTrueClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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--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 --- .../src/saas/services/supabaseClient.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frontend/editor/src/saas/services/supabaseClient.ts diff --git a/frontend/editor/src/saas/services/supabaseClient.ts b/frontend/editor/src/saas/services/supabaseClient.ts new file mode 100644 index 000000000..e3a91558f --- /dev/null +++ b/frontend/editor/src/saas/services/supabaseClient.ts @@ -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;