You know that feeling when users report a bug that makes absolutely no sense? “The app takes 3 minutes to load after I sign up.” Three minutes? That’s not a loading time, that’s a coffee break. This is the story of how I tracked down one of the weirdest bugs in DIALØGUE’s history.
Table of Contents
- The Mystery Begins
- The Investigation
- The Revelation
- Update: The Real Culprit and the Final, Robust Solution
- The Happy Ending
The Mystery Begins
It started innocently enough. A new user signed up using Google SSO, excited to try DIALØGUE. Then… nothing. Well, not exactly nothing – they got our beautifully designed loading skeleton. For three whole minutes.
The weird part? It only happened to new users. Existing users could log in instantly. And it wasn’t consistent – sometimes it was 1 minute, sometimes 3, occasionally it worked immediately.
My first thought: “Must be a cold start issue.” (Narrator: It wasn’t a cold start issue.)
The Investigation
Round 1: Blame the Frontend
```typescript
// First suspect: The profile loading hook
useEffect(() => {
if (user) {
fetchUserProfile(); // This was taking forever
}
}, [user]);
```
Added timers everywhere. The API call was indeed taking 3 minutes. But why? The backend should either return data or error out, not just… wait.
Round 2: Blame the Backend
Dove into our Supabase Edge Functions:
```typescript
// Edge Function for getting user profile
const { data: profile } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (!profile) {
// New user - create profile
await createUserProfile(userId);
}
```
This looked fine. Should be quick, right? Time to add more logging.
Round 3: The Plot Thickens
After adding logs everywhere (and I mean everywhere), I discovered something bizarre:
[00:00] User signs in with Google
[00:01] Auth trigger fires - creates user record
[00:01] Frontend requests profile
[00:01] Edge Function queries for user... NO RESULT
[00:02] Edge Function tries to create user...
[00:02] Database constraint error: User already exists
[00:03] Function retries...
[03:00] Function finally times out
Wait, what? The user doesn’t exist, but also already exists? Schrödinger’s user?
The Revelation
After staring at database logs until my eyes hurt, I finally saw it. Our database had competing processes:
- Supabase Auth Trigger: Creates user record on signup
- Edge Function: Tries to create user if not found
- Database Replication Lag: The trigger’s INSERT hadn’t replicated to the read replica yet
Here’s what was happening:
-- Auth trigger (on primary database)
INSERT INTO users (id, email) VALUES ($1, $2);
-- Edge Function (reading from replica)
SELECT * FROM users WHERE id = $1; -- Returns nothing!
-- Edge Function (trying to help)
INSERT INTO users (id, email) VALUES ($1, $2); -- CONFLICT!
The function would retry with exponential backoff, each attempt hitting the same race condition until either:
- The replication finally caught up (1-3 minutes)
- The function timed out (3 minutes)
Update: The Real Culprit and the Final, Robust Solution
After the initial post, we continued debugging, and while our backend changes were improvements, the root of the mystery remained elusive. The breakthrough came when we shifted our focus from the backend to the client-side hydration and authentication flow.
The Real Culprit: A Client-Side Race Condition
The problem wasn’t a slow database trigger or a cold Edge Function. The true issue was a classic client-side race condition:
- OAuth Redirect: A new user signs in with Google and is redirected back to our app.
- Async Session: The Supabase client library (
supabase-js
) starts processing the token from the URL to establish a session. This is an asynchronous process. - Premature Render: Our React app, however, renders immediately. It asks for the user’s session before the async process from step 2 is complete.
- The Failure: The app gets a
null
session, concludes the user isn’t logged in, and renders a blank or error state. A few moments later, the session becomes available, but it’s too late—the UI has already made its decision.
Our initial workarounds, like adding client-side retries, were just symptoms of fighting this fundamental race condition.
The Solution: A Single Source of Truth for Authentication
The correct and final solution was to re-architect our frontend authentication state management to be truly event-driven and robust.
1. Centralized Logic in SupabaseProvider
:
We refactored our SupabaseProvider
to be the single, authoritative source of truth for authentication. We removed all other listeners and checks from other hooks.
2. Using onAuthStateChange
Correctly:
The core of the fix was to rely exclusively on Supabase’s onAuthStateChange
listener.
// Simplified logic in SupabaseProvider.tsx
export function SupabaseProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // Start in a loading state
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
// We only consider authentication "finished" once this listener fires.
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
// ...
}
This pattern ensures the entire application remains in a loading
state until Supabase confirms the user’s session is either valid or null. There is no more race condition.
The Happy Ending
DIALØGUE now onboards new users in under a second. No more coffee breaks during signup. No more confused users wondering if they broke something.
The fix has been in production for 3 weeks now. Zero timeout issues. Zero race conditions. Just smooth, fast signups like it should have been from the start.
Was it worth spending a week debugging this? When I see new users seamlessly create their first podcast within minutes of signing up – absolutely.
Want to try the now-speedy signup? Create your AI podcast at DIALØGUE. I promise it won’t take 3 minutes anymore.
Chandler Nguyen is building DIALØGUE and learning that distributed systems will humble you every single time. Follow more adventures in debugging at chandlernguyen.com.