OTP for Email Confirmation Always Expired – Email Shows Confirmed but User Still Blocked

Hey Supabase team! I'm encountering a bug I can’t figure out.

Issue:
When a user clicks the email verification link, they are redirected to our app but see a message saying the OTP is expired. However, in the Supabase dashboard, the user shows as email confirmed with a valid confirmed_at timestamp.

Despite this, when the user tries to log in, they get an error saying “Email not confirmed” and are redirected back to the registration/login page due to our route protection middleware.

Expected behavior:
Once the email is confirmed via link, the user should be allowed to log in.

Current behavior:

User receives the confirmation email

Clicks the link → OTP expired error

Supabase dashboard shows email as confirmed

User can't log in because Supabase returns “Email not confirmed

Middleware redirects them back to /registration

Here’s a snippet from our middleware (updateSession):
const { data: { user }, } = await supabase.auth.getUser(); if (!user && !isPublicRoute) { return NextResponse.redirect(new URL('/registration', request.url)); }

And our login function:
const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error?.message === 'Email not confirmed') { router.push(/confirm-email?email=${encodeURIComponent(email)}); }
Question:

Why is the OTP considered expired even though the user gets marked as confirmed?

How can I ensure users don't get stuck in this loop?

Would appreciate any help or advice 🙏
Was this page helpful?