Login Flow (Supabase OAuth + Local Session)
This document is a future-facing operational reference for how login currently works in PATTTTERNS.
Scope
- Authentication model: client-side Supabase OAuth (Google, GitHub)
- Session storage: browser localStorage
- Callback path:
/auth/callback - No server session runtime (static-export friendly)
Entry Points
Every entry point outside the modal is provider-neutral (“Create account”) and only opens the modal. Provider selection happens in exactly one place — LoginModal — so no surface has to be updated when a provider is added or removed.
Entry points and the source they report to analytics:
| Entry point | source |
|---|---|
| Navbar button, desktop overflow menu, mobile menu | navbar |
| Pattern footer CTA | pattern_footer |
| My Library header | library |
/debug gate | debug_gate |
/debug/components regenerate gate | debug_components |
| Bookmark / chat / export / image gates | login_modal (trigger carries the detail) |
- User clicks a sign-in entry point →
openModal("manual", { source }). - User picks Google or GitHub in the modal; the app builds the OAuth URL for that provider.
- User completes provider consent.
- Supabase redirects browser to
/auth/callbackwith tokens in URL hash. - Callback page persists session and redirects to the original requested path.
Implementation Map
- OAuth URL builder and token/session utilities:
src/lib/auth-client.tsx
- Callback completion page:
src/app/auth/callback/page.tsx
- Auth context consumer/provider:
src/components/AuthSessionProvider.tsxsrc/lib/auth-client.tsx
- Provider selection UI and modal store:
src/components/LoginModal.tsxsrc/lib/auth-modal.ts
- Buttons:
src/components/SignInCtaButton.tsx(neutral, entry points)src/components/GoogleSignInButton.tsx,src/components/GitHubSignInButton.tsx(modal only)
Detailed Sequence
1) Sign-in trigger
- Entry points call
openModal("manual", { source }); only the modal callssignIn(provider). buildSupabaseOAuthUrl(provider, callbackUrl)builds a provider authorize URL.provideris"google" | "github"(theAuthProvidertype). The argument is required — there is no implicit Google fallback in the URL builder, so a new provider cannot be added without handling its parameters.- Provider-specific
scopes(and Google’sprompt=select_account) come fromOAUTH_PROVIDER_PARAMS. - Redirect target is encoded in
nextquery param on/auth/callback.
2) Provider callback
- Browser lands on
/auth/callbackwith:access_tokenrefresh_tokenexpires_inin URL hash.
3) Session finalization
completeSupabaseOAuthFromUrl(currentUrl)does:- parse hash tokens
- fetch user profile from Supabase
/auth/v1/user - write local session (
patttterns-auth-session) to localStorage - return success + next path
4) Redirect after login
- Callback page redirects to
nextpath on success. - Redirects to
/on failure.
5) Session restore on app load
restoreSupabaseSession()reads local session.- If token still valid, refreshes user profile.
- If expired, attempts refresh token flow.
- If refresh fails, clears local session.
Storage Contract
Local key:
patttterns-auth-session
Stored shape:
accessTokenrefreshTokenexpiresAtuser(normalized user object, includingproviderread from Supabaseapp_metadata.provider)
user.provider is what sign_in_completed analytics reports, since the completing page has no memory of which button started the flow.
Sign-out
signOutFromSupabase(accessToken)calls Supabase logout endpoint.- localStorage session is cleared.
Required Environment Variables
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY
Optional feature flags:
NEXT_PUBLIC_STATIC_LOGIN_MODENEXT_PUBLIC_BOOKMARK_SYNC_BACKEND
Product login gates
Soft conversion (see Soft Login Preview and Guest Chat):
- Guests may open Component Preview and the chat widget.
- Login modal on copy/download (
component_export) and on a second chat turn (chatbot). - Gemini guest quota is enforced in
chatbot-proxy, not in the browser.
Failure Modes
- Missing env vars
- Symptom: sign-in button appears but OAuth URL is null.
- Fix: set Supabase URL + anon key.
- Callback missing tokens
- Symptom: callback page shows failure and sends user home.
- Fix: verify provider and Supabase redirect URL configuration.
- Session silently lost
- Symptom: user appears logged out after reload.
- Fix: inspect refresh flow and ensure refresh token exchange succeeds.
- Wrong return path after login
- Symptom: users always return to home.
- Fix: verify
nextquery param propagation in sign-in trigger.
- GitHub user shows a blank display name
- Symptom: header shows no name after a GitHub sign-in.
- Cause: account has no public full name and a private email.
- Fix: confirm the
user:emailscope is requested and the GitHub provider in Supabase has it enabled;normalizeUserfalls back touser_name/preferred_username.
- Same-email user loses their library after switching provider
- Symptom: bookmarks and libraries vanish for a user who signed in with the other provider.
- Cause: Supabase issued a separate
auth.uid()instead of linking identities, and RLS scopes all rows byauth.uid(). - Fix: check the identity-linking setting in the Supabase dashboard; see Auth Setup.
Operational Notes
- This flow is intentionally browser-centric to stay compatible with static deployment.
- Keep auth logic centralized in
src/lib/auth-client.tsx; avoid duplicate token parsing in components. - For future migration to server sessions, preserve callback compatibility to avoid breaking existing links.