OTP Race Condition Fix Documentation
⚠️ CRITICAL: This Issue Has Been Fixed 6 Times - DO NOT BREAK IT AGAIN
Last Fixed: January 2025 (6th fix - server redirects) Previous Fixes:
- Fix 1-3: Original attempts (broken after cleanups)
- Fix 4: PostAuthHandler pathname check
- Fix 5: Extended to all auth pages
- Fix 6: FINAL FIX - Removed ALL client hooks from redirector pages
The Problem
When a user signs up using OTP (One-Time Password) verification in the dodev app, there's a race condition between:
- Clerk completing the OTP verification and redirecting to
/welcome - The backend webhook processing the new user and updating their metadata
- The client-side session being established
Symptoms
- Immediate after OTP entry: User sees "Application error: a client-side exception has occurred"
- Console errors:
- React Error #301 (invalid hook call outside provider)
- "Clerk authentication is not available"
isSignedIn: false,sessionId: "absent"in logs
- After refresh: Everything works fine (session is established)
The Root Cause
- User enters OTP code
- Clerk verifies OTP and immediately redirects to
/welcomewith URL parameters - PROBLEM: Clerk's session isn't established yet on the client side
- Components try to use auth hooks but the session doesn't exist
- React throws errors because hooks are called in an invalid state
The Solution
1. DO NOT Use Custom Auth Hooks in Auth Pages
❌ NEVER DO THIS in sign-in/sign-up pages:
// THIS WILL BREAK - DO NOT USE useAuth() in auth pages
import { useAuth } from "@/hooks/useAuth"
const { isAuthenticated } = useAuth() // BREAKS during OTP redirect✅ INSTEAD: Let Clerk components handle their own state:
// sign-in/[[...sign-in]]/page.tsx
import { SignIn } from "@clerk/nextjs"
export default function SignInPage() {
// NO auth hooks here - just render the SignIn component
return <SignIn fallbackRedirectUrl="/welcome" />
}2. Welcome Page MUST Use Clerk Hooks Directly
❌ AVOID custom auth hooks that might fail:
// Can fail if our wrapper isn't ready
import { useAuth } from "@/hooks/useAuth"✅ USE Clerk hooks directly - they handle edge cases better:
import { useClerk, useUser } from "@clerk/nextjs"
export default function WelcomePage() {
const clerk = useClerk()
const { user, isLoaded, isSignedIn } = useUser()
// These hooks are more resilient to the race condition
}3. Aggressive Post-OTP Detection and Reload
The welcome page MUST detect when it's in a post-OTP state and force a reload:
// Check if we just came from auth (OTP verification)
const urlParams = new URLSearchParams(window.location.search)
const hasClerkParams = urlParams.toString().includes('__clerk') ||
urlParams.toString().includes('__client') ||
urlParams.toString().includes('__session')
// If we have Clerk params but no session, we're in the race condition
if (hasClerkParams && userLoaded && !isSignedIn && !forceReload) {
console.log('[Welcome] Post-OTP race condition detected, forcing reload...')
setForceReload(true)
// Force reload to get fresh session from server
setTimeout(() => {
window.location.reload()
}, 500)
return
}4. Key Files and Their Roles
/app/sign-up/[[...sign-up]]/page.tsx
- MUST NOT use
useAuth()or any custom auth hooks - SHOULD only render the Clerk
<SignUp>component - USES
fallbackRedirectUrl="/welcome"to redirect after OTP
/app/sign-in/[[...sign-in]]/page.tsx
- MUST NOT use
useAuth()or any custom auth hooks - SHOULD only render the Clerk
<SignIn>component - USES
fallbackRedirectUrl="/welcome"to redirect after OTP
/app/welcome/page.tsx
- MUST use Clerk hooks directly:
useClerk(),useUser() - MUST detect post-OTP state via URL parameters
- MUST force reload when post-OTP detected but no session
- SHOULD show "Setting up your account..." during transition
/components/post-auth-handler.tsx
- Handles post-authentication redirects
- Forces reload after 2 seconds if user not synced
- Shows loading state during sync
What Breaks This Fix
1. Adding Auth Hooks to Sign-in/Sign-up Pages
DON'T: Try to check if user is already authenticated in sign-in/sign-up pages WHY: The auth context isn't ready during OTP redirect
2. Removing the Reload Logic from Welcome Page
DON'T: Remove the window.location.reload() thinking it's unnecessary
WHY: The reload is REQUIRED to get a fresh session from the server
3. Using Custom Auth Wrappers Too Early
DON'T: Wrap the welcome page in complex auth logic WHY: Custom wrappers might not handle the race condition properly
4. Changing ClerkProvider Configuration
DON'T: Add afterSignUpUrl or afterSignInUrl props to ClerkProvider
WHY: These are deprecated and can interfere with the redirect flow
5. ⚠️ CRITICAL: PostAuthHandler Must Skip ALL Auth-Related Pages
DON'T: Let PostAuthHandler use useAuth() hook on any auth-related pages
WHY: This causes React Error #301 because hooks are called before session is ready
FIX: PostAuthHandler MUST check pathname BEFORE calling any hooks and skip:
/sign-in,/sign-up(Clerk's auth pages)/login,/signup(Custom redirector pages - NO HYPHENS)/welcome(Post-OTP landing page)/unauthorized,/admin/login,/admin/unauthorized(Error and admin auth pages) NOTE: We have duplicate auth pages with and without hyphens - both must be skipped!
Testing the Fix
-
Sign up with OTP:
- Go to
/sign-up - Enter email
- Enter OTP code
- Should see "Setting up your account..." briefly
- Should land on welcome page without errors
- Go to
-
Check console:
- Should see:
[Welcome] Post-OTP race condition detected, forcing reload... - Should NOT see React Error #301
- Should NOT see "Application error"
- Should see:
-
Verify no manual refresh needed:
- User should NOT need to manually refresh the page
- Everything should work automatically
If It Breaks Again
- Check if someone added
useAuth()to sign-in/sign-up pages - Remove it - Check if welcome page reload logic was removed - Add it back
- Check if someone wrapped welcome page in new auth logic - Remove wrapper
- Check ClerkProvider props - Remove any deprecated redirect props
- Use this exact code for welcome page - See
/app/welcome/page.tsxfrom Jan 2025 fix
The Golden Rule
If the user just entered an OTP and there's no session yet, RELOAD THE PAGE.
Don't try to be clever. Don't try to wait for webhooks. Don't try to poll for updates. Just reload the page after 500ms and Clerk will have the session ready.
Related Files
Clerk Auth Pages (with hyphens - NO auth hooks)
/app/sign-up/[[...sign-up]]/page.tsx- Clerk sign up page/app/sign-in/[[...sign-in]]/page.tsx- Clerk sign in page
Custom Redirector Pages (without hyphens - FIXED: Now use server redirects)
/app/login/page.tsx- Server-side redirect to /sign-in (NO hooks)/app/signup/page.tsx- Server-side redirect to /sign-up (NO hooks)/app/admin/login/page.tsx- Server-side redirect to /sign-in (NO hooks)
Other Auth-Related Pages
/app/welcome/page.tsx- Welcome page (Clerk hooks only + reload logic)/app/unauthorized/page.tsx- Unauthorized access page/app/admin/unauthorized/page.tsx- Admin unauthorized page
Core Components
/components/post-auth-handler.tsx- Post-auth redirect handler (skips ALL auth pages)/components/client-layout-wrapper.tsx- Layout wrapper (simple, no conditional logic)/components/app-providers.tsx- ClerkProvider configuration/hooks/useAuth.ts- Custom auth hook (DO NOT use in ANY auth-related pages)
Summary
The OTP race condition happens because Clerk's session isn't ready immediately after OTP verification. The fix is:
- Don't use auth hooks in sign-in/sign-up pages
- Use Clerk hooks directly in welcome page
- Detect post-OTP state and force reload
- Show loading state during transition
This has been fixed 3 times. Please don't break it again.