fix: harden Supabase auth bootstrap to avoid import-time crash#10
fix: harden Supabase auth bootstrap to avoid import-time crash#10support371 merged 1 commit intomainfrom
Conversation
Reviewer's GuideMakes Supabase auth initialization and usage resilient to missing configuration by avoiding import-time crashes, exposing configuration status, and ensuring auth hooks and methods degrade gracefully when Supabase is unavailable. Sequence diagram for resilient Supabase auth initialization in AuthProvidersequenceDiagram
actor ReactTree
participant AuthProvider
participant SupabaseClientModule as SupabaseClient
participant SupabaseAuth as supabase.auth
ReactTree->>AuthProvider: mount
AuthProvider->>SupabaseClientModule: read isSupabaseConfigured, supabase, authUnavailableReason
alt Supabase not configured or supabase is null
AuthProvider->>AuthProvider: setSession(null), setUser(null)
AuthProvider->>AuthProvider: setIsLoading(false)
AuthProvider-->>ReactTree: provide context (isConfigured=false, authUnavailableReason)
else Supabase configured and supabase exists
AuthProvider->>SupabaseAuth: onAuthStateChange(handler)
SupabaseAuth-->>AuthProvider: subscription
AuthProvider->>SupabaseAuth: getSession()
alt getSession resolves
SupabaseAuth-->>AuthProvider: nextSession
AuthProvider->>AuthProvider: setSession(nextSession), setUser(nextSession.user)
AuthProvider->>AuthProvider: setIsLoading(false)
else getSession rejects
SupabaseAuth-->>AuthProvider: error
AuthProvider->>AuthProvider: setSession(null), setUser(null)
AuthProvider->>AuthProvider: setIsLoading(false)
end
AuthProvider-->>ReactTree: provide context (isConfigured=true, authUnavailableReason=null)
end
ReactTree->>AuthProvider: unmount
AuthProvider->>SupabaseAuth: subscription.unsubscribe()
Sequence diagram for guarded auth method calls when Supabase is unavailablesequenceDiagram
actor UserComponent
participant AuthProvider
participant SupabaseClientModule as SupabaseClient
participant SupabaseAuth as supabase.auth
UserComponent->>AuthProvider: signIn(email, password)
AuthProvider->>SupabaseClientModule: check isSupabaseConfigured and supabase
alt Supabase not configured or supabase is null
AuthProvider-->>UserComponent: { error: getConfigError() }
else Supabase configured
AuthProvider->>SupabaseAuth: signInWithPassword(email, password)
SupabaseAuth-->>AuthProvider: { error }
AuthProvider-->>UserComponent: { error }
end
UserComponent->>AuthProvider: signUp(email, password, metadata)
AuthProvider->>SupabaseClientModule: check isSupabaseConfigured and supabase
alt Supabase not configured or supabase is null
AuthProvider-->>UserComponent: { error: getConfigError(), needsEmailConfirmation: false }
else Supabase configured
AuthProvider->>SupabaseAuth: signUp(email, password, redirectUrl, metadata)
SupabaseAuth-->>AuthProvider: { data, error }
AuthProvider->>AuthProvider: needsEmailConfirmation = not error and not data.session
AuthProvider-->>UserComponent: { error, needsEmailConfirmation }
end
UserComponent->>AuthProvider: resetPassword(email)
AuthProvider->>SupabaseClientModule: check isSupabaseConfigured and supabase
alt Supabase not configured or supabase is null
AuthProvider-->>UserComponent: { error: getConfigError() }
else Supabase configured
AuthProvider->>SupabaseAuth: resetPasswordForEmail(email, redirectTo)
SupabaseAuth-->>AuthProvider: { error }
AuthProvider-->>UserComponent: { error }
end
UserComponent->>AuthProvider: updatePassword(password)
AuthProvider->>SupabaseClientModule: check isSupabaseConfigured and supabase
alt Supabase not configured or supabase is null
AuthProvider-->>UserComponent: { error: getConfigError() }
else Supabase configured
AuthProvider->>SupabaseAuth: updateUser(password)
SupabaseAuth-->>AuthProvider: { error }
AuthProvider-->>UserComponent: { error }
end
UserComponent->>AuthProvider: signOut()
AuthProvider->>SupabaseClientModule: check isSupabaseConfigured and supabase
alt Supabase not configured or supabase is null
AuthProvider-->>UserComponent: return without calling supabase
else Supabase configured
AuthProvider->>SupabaseAuth: signOut()
SupabaseAuth-->>AuthProvider: result
AuthProvider-->>UserComponent: void
end
Updated class diagram for Supabase client and AuthContext typesclassDiagram
class SupabaseClientModule {
+string SUPABASE_URL
+string SUPABASE_PUBLISHABLE_KEY
+boolean isSupabaseConfigured
+string authUnavailableReason
+SupabaseClient supabase
}
class SupabaseClient {
+SupabaseAuth auth
}
class SupabaseAuth {
+onAuthStateChange(handler) subscription
+getSession() Promise
+signInWithPassword(email, password) Promise
+signUp(email, password, redirectUrl, metadata) Promise
+resetPasswordForEmail(email, options) Promise
+updateUser(password) Promise
+signOut() Promise
}
class AuthContextType {
+User user
+Session session
+boolean isLoading
+boolean isConfigured
+string authUnavailableReason
+signIn(email, password) Promise
+signUp(email, password, metadata) Promise
+resetPassword(email) Promise
+updatePassword(password) Promise
+signOut() Promise
}
class AuthProvider {
-User user
-Session session
-boolean isLoading
+getConfigError() Error
+AuthProvider(children)
}
SupabaseClientModule --> SupabaseClient : supabase (nullable)
SupabaseClient --> SupabaseAuth : auth
AuthProvider --> AuthContextType : provides
AuthProvider --> SupabaseClientModule : uses exports isSupabaseConfigured, supabase, authUnavailableReason
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- Since
supabaseis guaranteed to benullwhenisSupabaseConfiguredis false, you can simplify the repeated guards (if (!isSupabaseConfigured || !supabase)) to just checkif (!supabase)and rely on the single source of truth from the client module. authUnavailableReasonis only non-null when Supabase is misconfigured, sogetConfigErrordoesn’t need the||fallback; usingauthUnavailableReason!or inlining the message inclient.tsas a constant would make the relationship between the two clearer and avoid redundant messaging logic.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Since `supabase` is guaranteed to be `null` when `isSupabaseConfigured` is false, you can simplify the repeated guards (`if (!isSupabaseConfigured || !supabase)`) to just check `if (!supabase)` and rely on the single source of truth from the client module.
- `authUnavailableReason` is only non-null when Supabase is misconfigured, so `getConfigError` doesn’t need the `||` fallback; using `authUnavailableReason!` or inlining the message in `client.ts` as a constant would make the relationship between the two clearer and avoid redundant messaging logic.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 686b665c6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }) | ||
| : null; |
There was a problem hiding this comment.
Avoid exporting a null Supabase client
Returning null here makes missing-config mode crash again in code paths that still assume supabase is always a client (for example src/pages/ResetPassword.tsx:29 calls supabase.auth.onAuthStateChange and src/pages/Contact.tsx:75 calls supabase.functions.invoke without guards). Because strictNullChecks is disabled, these call sites are not type-checked and will fail at runtime with Cannot read properties of null, which undermines the goal of a controlled fallback when env vars are absent.
Useful? React with 👍 / 👎.
| export const supabase = isSupabaseConfigured | ||
| ? createClient<Database>(SUPABASE_URL!, SUPABASE_PUBLISHABLE_KEY!, { | ||
| auth: { | ||
| storage: localStorage, | ||
| persistSession: true, | ||
| autoRefreshToken: true, | ||
| } | ||
| }) | ||
| : null; |
There was a problem hiding this comment.
🔴 ResetPassword.tsx crashes at runtime when supabase is null due to incomplete null-guard migration
The PR changes supabase from always-defined to SupabaseClient | null (src/integrations/supabase/client.ts:22-30), but src/pages/ResetPassword.tsx:29 still calls supabase.auth.onAuthStateChange(...) directly inside a useEffect without a null check. Since /reset-password is a public route with no auth guard (src/App.tsx:99), navigating to it when Supabase is unconfigured causes a TypeError: Cannot read properties of null (reading 'auth'). This will crash the component (or trigger the app-level ErrorBoundary, showing a generic "Something went wrong" page). Since strictNullChecks is false in tsconfig.app.json:20, TypeScript does not catch this at compile time. CLAUDE.md explicitly requires password reset to work and demands "no silent auth failures" and a "readable auth/config failure state."
Prompt for agents
In src/pages/ResetPassword.tsx, the useEffect at line 29 calls supabase.auth.onAuthStateChange() directly without a null check. Since supabase can now be null (changed in src/integrations/supabase/client.ts:22-30), this needs a guard. Add an early return in the useEffect if supabase is null, similar to the pattern used in src/hooks/useAuth.tsx:29-34. You should also check isSupabaseConfigured before the call. The same pattern should also be applied to Contact.tsx:75 (supabase.functions.invoke) and all functions in src/hooks/useBlog.ts that call supabase.from() directly, as these are also used on public routes (/contact, /blog, /blog/:slug) and will crash the same way. For useBlog.ts, either add null guards inside each queryFn or add an `enabled: !!supabase` condition to each useQuery/useMutation call.
Was this helpful? React with 👍 or 👎 to provide feedback.
PR #10 branched before the onboarding redirect fix was on main, so it carried the old ${origin}/ value. Restore to ${origin}/kyc so email-confirmed users land on the KYC form, not the public homepage. https://claude.ai/code/session_01Gn5FmGBZL1etB2kShwaNyX
- client.ts: no longer throws at module load when env vars are missing; exports isSupabaseConfigured + authUnavailableReason; supabase is null when unconfigured (console.warn only) - useAuth.tsx: guards all auth methods against null supabase; isLoading always settles (including .catch path); exposes isConfigured + authUnavailableReason in context; emailRedirectTo restored to /kyc https://claude.ai/code/session_01Gn5FmGBZL1etB2kShwaNyX
…e Safari blank render
Root cause: framer-motion v12.26.2 silently fails to fire its animation
engine on mobile Safari. Every homepage element used initial={{ opacity: 0 }}
with animate={{ opacity: 1 }}, meaning ALL visible content remained invisible
when the animation engine stalled. Only the raw CSS background layers
(gradient mesh, cyber-grid, blur blobs) were visible — the reported
"dark gradient shell" symptom.
Files changed:
- HeroSection.tsx: remove framer-motion import; replace all motion.div /
motion.h1 / motion.p with plain HTML + animate-fade-in class + inline
animation-delay for stagger; remove unused Eye import
- BentoGrid.tsx: remove motion import; plain div on BentoCard and section
header; all hover effects preserved via Tailwind group/CSS transitions
- StatsSection.tsx: remove motion import and framer-motion useInView;
replace with native IntersectionObserver (threshold 0.1) to trigger the
counter animation; remove motion.div wrappers on uptime card and stat cards
- CTASection.tsx: remove motion import; replace single motion.div CTA box
with plain div — no animation needed on a call-to-action
- GemAssist.tsx: remove motion and AnimatePresence imports; floating button
becomes a plain <button> with Tailwind hover:scale / active:scale;
chat panel uses conditional render + animate-scale-in CSS class instead
of AnimatePresence; message items use animate-fade-in
All CSS animations use keyframes already defined in tailwind.config.ts
(fade-in, scale-in) and index.css (slide-up). These are native CSS and
work reliably across all browsers including mobile Safari.
Auth/bootstrap hardening from PR #10 is untouched.
https://claude.ai/code/session_01Gn5FmGBZL1etB2kShwaNyX
PR #10 branched before the onboarding redirect fix was on main, so it carried the old ${origin}/ value. Restore to ${origin}/kyc so email-confirmed users land on the KYC form, not the public homepage. https://claude.ai/code/session_01Gn5FmGBZL1etB2kShwaNyX
Summary
isSupabaseConfiguredandauthUnavailableReasonuseAuthso loading always settles and config errors do not crash the appWhy
The merged production branch still had an import-time crash path in
src/integrations/supabase/client.tsandsrc/hooks/useAuth.tsx. IfVITE_SUPABASE_URLorVITE_SUPABASE_PUBLISHABLE_KEYis missing, the React tree could fail before auth state resolves.Result
Summary by Sourcery
Harden Supabase auth bootstrap so missing configuration no longer crashes the app at import time and auth flows can fail gracefully.
New Features:
Bug Fixes:
Enhancements: