Internal DocumentationArchived DocumentationDo dev legacyArchiveClerk migration

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:

  1. Clerk completing the OTP verification and redirecting to /welcome
  2. The backend webhook processing the new user and updating their metadata
  3. 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

  1. User enters OTP code
  2. Clerk verifies OTP and immediately redirects to /welcome with URL parameters
  3. PROBLEM: Clerk's session isn't established yet on the client side
  4. Components try to use auth hooks but the session doesn't exist
  5. 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

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

  1. 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
  2. Check console:

    • Should see: [Welcome] Post-OTP race condition detected, forcing reload...
    • Should NOT see React Error #301
    • Should NOT see "Application error"
  3. Verify no manual refresh needed:

    • User should NOT need to manually refresh the page
    • Everything should work automatically

If It Breaks Again

  1. Check if someone added useAuth() to sign-in/sign-up pages - Remove it
  2. Check if welcome page reload logic was removed - Add it back
  3. Check if someone wrapped welcome page in new auth logic - Remove wrapper
  4. Check ClerkProvider props - Remove any deprecated redirect props
  5. Use this exact code for welcome page - See /app/welcome/page.tsx from 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.

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)
  • /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:

  1. Don't use auth hooks in sign-in/sign-up pages
  2. Use Clerk hooks directly in welcome page
  3. Detect post-OTP state and force reload
  4. Show loading state during transition

This has been fixed 3 times. Please don't break it again.

On this page