PURPOSE
Single Supabase client instance for the entire app, plus the canonical invokeRpc() helper that every server-authoritative mutation flows through. Validates required env vars at import time so misconfigured builds fail loudly rather than silently.
OWNS
- The exported
supabaseclient (created viacreateClientfrom@supabase/supabase-js). - The
invokeRpc<T>()async helper that wrapssupabase.rpc()with error extraction, typed returns, offline short-circuit, and Sentry breadcrumb emission. - Import-time validation of
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEY. - The
isOffline()lazy lookup that reads the__PLAYER_STORE_OFFLINE__flag offwindowto avoid a circularplayerStore -> supabase -> playerStoreimport graph.
READS FROM
import.meta.env.VITE_SUPABASE_URLandimport.meta.env.VITE_SUPABASE_ANON_KEY(Vite env, validated at module load).window.__PLAYER_STORE_OFFLINE__global flag mirrored from the zustand player store.localStorage(passed as the auth session storage tocreateClient).
PUSHES TO
- Supabase RPC endpoints via
supabase.rpc(functionName, params). - Sentry as
addBreadcrumbentries (categorysupabase, levelerror) when an RPC errors. console.warnwhen an RPC is skipped because the user is in offline mode.
DOES NOT
- Does not import the service role key — anon key only, service role is server-side.
- Does not bypass RLS — security is enforced server-side via
SECURITY DEFINERRPCs. - Does not call
supabase.from()directly outside ofauth.ts(which uses it for session management only). - Does not send Sentry events for RPC failures (breadcrumbs only) to avoid one-event-per-call issue spam during production outages.
- Does not import from
playerStoredirectly — uses thewindow.__PLAYER_STORE_OFFLINE__global to break the circular dependency. - Does not provide a non-null default for offline short-circuits — callers needing one must check
usePlayerStore.getState().offlineModethemselves. - Does not persist anything beyond what the Supabase JS client persists to
localStoragefor session.
Signals
- Throws
Error('VITE_SUPABASE_URL not set — check .env.development')at import time if URL env is missing. - Throws
Error('VITE_SUPABASE_ANON_KEY not set — check .env.development')at import time if anon key env is missing. - Throws
Error('RPC <name> failed: <message>')when an RPC returns an error. - Emits Sentry breadcrumb
{ category: 'supabase', level: 'error', message: 'RPC <name> failed', data: { endpoint, message } }before throwing. - Emits
console.warn('[offline] skipping rpc:', functionName)on offline-mode short-circuit. - Returns
nullcast toTwhen offline mode is active.
Entry points
supabase— exported Supabase client used directly byauth.tsfor session management.invokeRpc<T>(functionName, params?)— exported helper imported by every server-authoritative mutation in the metagame layer.
Pattern notes
- Fail-fast env validation. Missing env vars throw at module import, not on first use, so a misconfigured build never reaches runtime in a half-working state.
- Single client. One
createClientcall at module top level; consumers import the shared instance rather than constructing their own. - All mutations through
invokeRpc. Direct.from()chains are restricted toauth.ts. This keeps the RPC surface auditable and gives one place to add cross-cutting concerns (offline guard, breadcrumbs, error formatting). - Breadcrumbs not events. Failed RPCs add a Sentry breadcrumb so a downstream uncaught throw carries the RPC context, without flooding Sentry with one issue per failed call during an outage.
- Offline guard via global, not import.
isOffline()readswindow.__PLAYER_STORE_OFFLINE__instead of importingplayerStoreto avoid a circular dependency between the client module and the store that consumes it. - Generic-typed return.
invokeRpc<T>is generic withT = unknowndefault; callers narrow with their own row/payload types at the call site. auth.persistSession: truewithlocalStoragekeeps a signed-in user across reloads on the same device.