auth.ts
PURPOSE
Authentication service that wraps Supabase Auth. Manages the three account states (guest_anonymous, claimed_email, signed_out) and the magic-link claim flow that upgrades an anonymous identity into a permanent email-linked account.
OWNS
- The cold-start session contract:
ensureAnonymousSession()either restores an existing session or creates an anonymous user. - Magic-link redirect detection for both PKCE (
?code=query) and implicit (#access_token=hash) flows. - The hard timeout that prevents the splash from hanging on an unreachable Supabase backend (
AUTH_TIMEOUT_MS = 5000). - URL cleanup after magic-link tokens are consumed (
window.history.replaceState). - Subscription/unsubscription bookkeeping for the auth-state listener.
READS FROM
./supabase— the Supabase client singleton.@supabase/supabase-js—Usertype.window.location.search/window.location.hash— magic-link redirect parameters.window.location.origin/window.location.pathname— redirect target and URL cleanup.- Supabase session state via
supabase.auth.getSession()andsupabase.auth.getUser().
PUSHES TO
- Supabase Auth:
signInAnonymously,signInWithOtp,signOut,refreshSession,onAuthStateChange. window.history— clears the magic-link token from the URL after consumption.- Callers registered via
onAuthChange— fired on every Supabase auth state change with the resolvedUser | null.
DOES NOT
- Persist user profile, display name, or any non-Supabase identity data.
- Talk to the database directly — only Supabase Auth endpoints.
- Render any UI; surfaces errors by throwing.
- Retry on failure; a Supabase timeout aborts with
SUPABASE_TIMEOUT: auth unreachable. - Track local state — every call asks Supabase fresh.
- Handle offline mode — the caller is expected to catch the timeout and offer the offline escape hatch.
- Validate emails before sending magic links.
Signals
- Throws
Error('SUPABASE_TIMEOUT: auth unreachable')fromensureAnonymousSessionif Supabase does not respond withinAUTH_TIMEOUT_MS. - Throws
Error('Anonymous sign-in returned no user')if Supabase reports success but returns no user object. - Re-throws any error returned by Supabase Auth from
signInAnonymously,signOut,sendMagicLink,refreshSession,getSession. onAuthChangecallbacks receiveUser | nullonSIGNED_IN,TOKEN_REFRESHED,SIGNED_OUT, and other Supabase auth state transitions._waitForMagicLinkSessionresolves with the authenticated user onSIGNED_INorTOKEN_REFRESHED, ornullafter an 8s internal timeout.
Entry points
ensureAnonymousSession(): Promise<User>— call once on cold start; restores or creates a session, races against a 5s timeout.signInAnonymously(): Promise<void>— explicit anonymous sign-in.signOut(): Promise<void>— clears the session.getUser(): Promise<User | null>— current user, or null if signed out.sendMagicLink(email: string): Promise<void>— initiates email claim flow; passesshouldCreateUser: trueand an explicitemailRedirectTo: window.location.origin.refreshSession(): Promise<User | null>— refresh after magic-link redirect to pick up upgraded identity.getSession(): Promise<Session | null>— current session including tokens.onAuthChange(callback): () => void— subscribe to auth state changes; returns an unsubscribe function.
Pattern notes
ensureAnonymousSessionusesPromise.raceagainst anAUTH_TIMEOUT_MS(5000) timeout so the splash never freezes on a dead Supabase instance.- Magic-link handling is bifurcated: PKCE flow lands with
?code=inwindow.location.search; older implicit flow lands withaccess_token/refresh_tokeninwindow.location.hash. Either pattern triggers_waitForMagicLinkSessionbefore falling back togetSession. _waitForMagicLinkSessionwaits ononAuthStateChangeforSIGNED_INorTOKEN_REFRESHED, with an 8s safety timeout that resolves tonullrather than hanging app boot.- After a successful magic-link consumption,
window.history.replaceState(null, '', window.location.pathname)strips the tokens from the URL so a refresh does not re-trigger the flow. sendMagicLinksetsshouldCreateUser: trueso an anonymous user clicking the link upgrades their existing identity rather than creating a fresh account.emailRedirectTois set explicitly towindow.location.originto avoid Supabase defaulting to localhost when the Site URL is misconfigured.- All exported async functions either return the requested value or throw — no silent-null fallbacks except
getUserandrefreshSession, which returnnullto represent the signed-out state. onAuthChangereturns a cleanup function that defensively readsdata?.subscription?.unsubscribe, tolerating a missing subscription on unexpected Supabase responses.