Details below are from Supabase’s documentation on 23 October 2026. Check it for current limits and settings. For the basics, sending your first test emails and setting up a sender, start with Test Supabase Auth emails.
The email never arrives
The built-in sender won’t email your testers. Supabase’s own email service only delivers to members of your project’s team, at most 2 emails an hour. It works for you because you’re on the team, then refuses everyone else with “Email address not authorized”. Set up custom SMTP under Authentication, Emails.
Your SMTP provider is still in its sandbox. A new Resend account sends only to your own address until you verify a domain, and Amazon SES only to addresses you’ve verified. Supabase hands the email over, and the provider quietly drops it.
You asked again too soon. By default each user can get one confirmation, magic-link, code or reset email every 60 seconds, and each IP address can make 30 sign-up and sign-in requests every 5 minutes. A test that retries quickly, or a suite running from one CI machine, hits these and gets a 429. Raise them for your staging project under Authentication, Rate Limits.
The address already has an account. To avoid revealing who has signed up, Supabase answers a sign-up for a confirmed address as if it succeeded, with a user that has no identities, and sends no email. A test that reuses an address waits for an email that never comes. Use a fresh address for every run.
Confirm email is switched off. Then sign-up signs the user in at once and sends nothing. Check Authentication, Sign In / Providers, Email. It’s often turned off while building and never back on.
The link says it’s invalid or has expired
You click the link and land on your app with error_code=otp_expired (“Email link is invalid or has expired”) in the address bar.
- It was used already. Links work once and last an hour by default. Asking for another email replaces the earlier link, so use the newest one.
- A scanner opened it first. Some email services, and company security tools such as link scanners, open every link in a new email to check it. That uses up a one-time link before the person clicks it. Supabase suggests sending a 6-digit code instead (
{{ .Token }}in the template) or linking to your own page where the person presses a button to confirm. Proxy Mail QA never opens links, so if a link works in your test but fails for real users, a scanner is the likely cause.
The link opens, but nobody is signed in
It was opened in a different browser. With the PKCE flow, which Supabase’s server-side helpers use, the browser that started the sign-up keeps a secret the link needs. Open the link in another browser, a private window or another device, and the exchange fails, often as “both auth code and code verifier should be non-empty”. Testers should open the link in the browser they signed up in. For links that work anywhere, build them from {{ .TokenHash }} and verify them on your server with verifyOtp.
Nothing finishes the sign-in. The link lands on your app with a code (or a token) in the address, and some page has to exchange it for a session. If that route is missing, the person sees your home page, signed out. Check the link’s destination page calls exchangeCodeForSession, or verifyOtp with the token hash.
The server can’t see the session. The default link puts the session after a # in the address, which only the browser can read. Server-rendered apps need the token-hash link above.
Links go to localhost or the wrong environment
The Site URL starts as http://localhost:3000, and any redirectTo that isn’t on the Redirect URLs list falls back to it. Set the Site URL to the deployed app and list every environment. More in why emails link to localhost; Proxy Mail QA flags those links on the address’s row.
Sign-up fails with “Database error saving new user”
Something in the database refused the new user, almost always a trigger on auth.users, such as one that creates a row in profiles. Common causes: the table or a column it writes to doesn’t exist, the function isn’t security definer and so can’t write to public, or a column it fills is not null and the sign-up didn’t send it. Supabase’s Auth and Postgres logs show the real error.
Test with awkward values too: a name with an apostrophe, another script or 255 characters can break a profile column that ordinary test data never reaches. Unusual names and passwords has a list to try.
The reset email arrives, but the password doesn’t change
A reset link signs the person in with a recovery session and fires the PASSWORD_RECOVERY event. Your app has to notice that, show a form for the new password and call updateUser with it. If it doesn’t, the person just ends up signed in, with their old password. With PKCE, the link must also be opened in the browser that asked for it. More in Test password reset emails.
Captcha blocks your automated tests
With captcha protection on (Authentication, Attack Protection), every sign-up and sign-in needs a captcha token, so an automated browser can’t get through. On your staging project, use your captcha provider’s test keys, which always pass, and keep the real keys for production.
It works on local Supabase but not on the hosted project
The Supabase CLI catches local email in its own mail viewer (usually at localhost:54324) and reads templates from supabase/config.toml. The hosted project uses the dashboard’s templates, sender and rate limits. Test on a hosted staging project before launch, with real email to an address that receives it.
Check them all in one pass
- Create a fresh Proxy Mail QA address for each scenario: new user, existing user, password reset, magic link.
- Go through the flow in your app. The code or link appears on the address’s row within seconds.
- Look at the row: a link to localhost is flagged, and the inbox shows whether the email says when its link expires.
- Open the link in the same browser you signed up in, and check you’re signed in on the right site.
Built with Lovable, Bolt or v0? Problems in vibe-coded sign-up and login covers what those apps usually get wrong on top of these.