Skip to content
FlowPatrol

Guide

Why are Supabase sign-up confirmation emails not arriving?

The form still submits, and the confirmation email never shows up. Sometimes Supabase Auth never sent it. Sometimes the message is in spam. Sometimes it arrived and the link points at localhost, a preview host, or a path the last edit deleted. Read the signup response before you rewrite the form. An edit to the Lovable, Bolt, v0, Replit, or Cursor app does not, by itself, change the Supabase email settings.

This is not a launch checklist. For signup, login, and checkout on a published Lovable URL, use how to test a Lovable app before launch. If the form never signs the user in, the key is wrong, or the session dies on reload, use why Supabase login stopped working after an AI edit. This page is only the confirmation message. Password reset and magic links use the same sender and the same URL allow list. The login version of a redirect that bounces to the wrong host is in login works locally but breaks in production.

Read the signup response before you check the inbox

Use the published URL in a private window, with an address you can open. The editor preview is a different origin. A confirmation sent from there asks to return to the preview, not to the domain you shipped. If the request host is not the Supabase project open in your dashboard, you are reading the wrong logs. That mismatch is the public-key section of the Supabase login guide.

  1. Submit the form. In the network panel, open the POST to /auth/v1/signup on your-project.supabase.co.
  2. 200 with a user and a null session means Auth accepted the signup. When Confirm email is on, it tried to send mail. Check spam, then Logs, then Auth, if the inbox is empty.
  3. 200 with a session means Confirm email is off, so no confirmation is sent. A check-your-email screen is waiting for a message this project will not send.
  4. Email address not authorized, code email_address_not_authorized, means the built-in sender refused that address. 429 and email rate limit exceeded, code over_email_send_rate_limit, means the hourly cap. A second signup for the same user can also wait: that limit defaults to 60 seconds.
  5. 500, or a body that says Error sending confirmation email, means the mailer failed. Read the Auth log line before you change the form.
  6. No request to /auth/v1/signup means the button is not calling signUp. signInWithPassword and signInWithOtp do not send this confirmation. The login guide covers a form that calls the wrong method.
  7. If the address is already under Authentication, then Users, a second signup does not send another confirmation. Resend with supabase.auth.resend, type signup, and the same emailRedirectTo. That call uses the same cap and the same allow list.

Confirm email is off, so nothing is sent

On a hosted Supabase project, email confirmation starts on. On a local or self-hosted project, it starts off. The toggle is Confirm email, on the Email provider: Authentication, then Sign In / Providers. A code edit does not flip it. A pushed Supabase config file can. In supabase/config.toml the setting is enable_confirmations under [auth.email].

  1. When the toggle is on, signUp returns a user and no session until the link is used. The confirmation time under Authentication, then Users, stays empty, and signInWithPassword says the email is not confirmed.
  2. When the toggle is off, signUp can return a session and Auth sends nothing. Use that session, or turn the toggle on and show check-your-email only when the session is null. A form that does not match the toggle is the login guide.
  3. Turning Confirm email off so the missing message “goes away” lets someone register an address they do not own, and Auth marks it confirmed. Turn it back on before launch.

The built-in sender refused the address or hit its cap

Until you save custom SMTP, Supabase Auth sends through its own mail server. That server is for trying Auth out, not for production, and Supabase does not promise delivery or uptime for it.

  1. It mails only organization team addresses. Anyone else is refused with Email address not authorized. Members are listed in the organization’s Team settings. The address you typed into the form is refused unless it is one of those members.
  2. The project is capped at 2 messages per hour on this sender. Supabase says the cap can change without notice. Signup, password reset, magic links, and invites share it. Editor signups spend the hour before you try the published URL. Wait, or use a team address you have not already mailed, before you blame the template.
  3. That hourly cap applies while Confirm email is on. Turning the toggle off removes the cap and is still not a production fix.

Production needs your own SMTP

Production mail goes through your own SMTP server, or a Send Email hook if the provider has no SMTP. Any service that speaks SMTP can work. Supabase’s guide names Resend, Amazon SES, Postmark, Twilio SendGrid, ZeptoMail, and Brevo as examples. The fields are under Authentication, then Emails, then SMTP Settings: host, port, username, password, sender name, and the From address.

  1. After you save, Auth will send to addresses that are not on the team. A default cap of 30 messages per hour then applies. Change it under Authentication, then Rate Limits, before a public launch. Saving SMTP does not change the template or the redirect allow list.
  2. The From address has to be one that service is allowed to send. The password stays in the Supabase SMTP settings, not in the React app and not in the editor chat.
  3. A supabase/config.toml block under [auth.email.smtp] does not change the hosted project until that config is pushed. If it was pushed, that block is the sender.
  4. An Auth log that names a bad password or a TLS error against your host means the provider rejected the handoff. Fix host, port, username, and password on that provider’s SMTP page. Do not rewrite signUp while the log still names the provider.

Site URL or the redirect allow list still points at the old host

This is the confirmation link’s return address, not the OAuth provider callback. Google and GitHub redirect URIs are in the login guide. Open Authentication, then URL Configuration.

  1. Site URL is where Auth sends the browser when signUp does not pass emailRedirectTo. It defaults to http://localhost:3000. Left there, or left on an editor preview host, the confirmation follows that origin even though the person signed up on the published domain.
  2. emailRedirectTo has to match a Redirect URLs entry, including https and the path. Supabase’s note on redirectTo is that the exact URL has to be in that list or the click does not use it, and the browser lands on Site URL instead. That is why a production signup ends at localhost.
  3. A trailing slash, a www host, and a new path are different URLs. https://yourapp.com/auth/confirm does not match the same path with a trailing slash, or the www host. An asterisk does not match a slash, so an entry ending in /* misses a nested path. Use /** for preview hosts. Supabase recommends the exact path in production.
  4. After a custom domain, add the published origin and the callback path the code uses now, including www if both hosts serve the app. Changing the domain in the editor does not update this list. Set Site URL before you widen the list. It is the default for every confirmation that omits emailRedirectTo.

The template link no longer confirms the address

The Confirm signup template is under Authentication, then Emails, then Templates. It can also be an HTML file under supabase/templates, pointed at by config.toml as auth.email.template.confirmation. An AI edit of that file is in the diff. A dashboard edit is not.

  1. {{ .ConfirmationURL }} is the verification link. It points at https://project-ref.supabase.co/auth/v1/verify with the token, type=email, and redirect_to. Replacing it with localhost, a preview URL, or a deleted path sends a message that arrives and does not confirm.
  2. {{ .SiteURL }} is only the Site URL. Linking the button there drops the token. {{ .RedirectTo }} is the redirect passed to signUp, and it is not a verification link on its own. {{ .Token }} is the one-time code. If the template shows only that code and the app has nowhere to type it, the message is useless.
  3. The documented server-side link uses {{ .TokenHash }}: {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email&next={{ .RedirectTo }}. If Site URL changed, or the edit removed /auth/confirm, the click 404s.
  4. Some mail tools open {{ .ConfirmationURL }} before the person does. Supabase documents this for scanners such as Microsoft Defender for Office 365 Safe Links. The click then says the token has expired or is invalid. That is a consumed link, not a missing email. Send the person to a page on your site that confirms on a second click, or have them type the code.

The edit changed signUp or the callback route

Search for signUp and emailRedirectTo. The option is options.emailRedirectTo on supabase.auth.signUp. If it is missing, Auth uses Site URL. If the edit set it to window.location.origin, a signup from the editor preview asks to return to the preview. Prefer the published origin.

  1. Read the URL the link opens. A fragment with access_token is the implicit flow, the JavaScript default for a client-only app. The page has to read that fragment. detectSessionInUrl set to false ignores it. The login guide covers that flag.
  2. A code query parameter has to be passed to exchangeCodeForSession. Server-rendered apps use this PKCE flow, and the email does not include the access token. token_hash and type have to be passed to verifyOtp. A template that sends one shape and a route that reads the other is the break. The exchange call is the same one the login guide describes for OAuth. This page is the template and emailRedirectTo that point at it.
  3. Moving /auth/confirm to /auth/callback, or the reverse, has to update the template or emailRedirectTo and the Redirect URLs list together. The old path 404s. Auth still sent the message.

The message was sent, and the inbox filed it as spam

If the signup response is 200, the user row exists, and Logs, then Auth, does not name a send error, the message left Auth. Check spam, junk, and the promotions tab, and check the address on the user row against the inbox you opened. A typo or a plus-tag is a different mailbox.

  1. On the built-in sender, links point at your project’s supabase.co host. Supabase recommends a custom domain for Auth, because a bad reputation on some other project can pull mail that uses the shared host toward spam. Custom SMTP is still the production path.
  2. With your own SMTP, the From domain needs the DKIM, DMARC, and SPF records that sending service gives you. Do not paste a generic SPF record from a forum. A new domain with none of the three is a common reason the provider accepted the handoff and the inbox never shows the message.
  3. Keep Auth mail on its own From address, separate from marketing. A template the edit turned into a newsletter, with extra links or images, is easier for a filter to drop. Put it back to a short message and one verification link.

Check the whole path once

  1. Use a new address the sender is allowed to mail. On the built-in sender, that is an organization team address, and this hour’s cap is not already spent. Private window, published URL, not the preview.
  2. Match the /auth/v1/signup status to the first section. Then open the message. The link should be your project’s supabase.co /auth/v1/verify URL, or your own /auth/confirm route. Localhost or an old preview host means Site URL, emailRedirectTo, or the template is still wrong.
  3. Click it once. The browser should land on the published origin, on a route that exists, and the user row should gain a confirmation time. signInWithPassword should no longer say the email is not confirmed.
  4. Reload. You should still be signed in. If the row is confirmed and the app bounced to the form, the session was not stored. That is the login guide, not the mailer.

A prompt you can paste

Paste this into Lovable, Bolt, v0, Cursor, or the editor you used, after you have the signup response or the Auth log line. Replace the brackets. It keeps the edit on the confirmation email.

Sign-up confirmation emails are not arriving on my published app after the last edit. Do not change unrelated pages, checkout, or layout. Do not turn off Confirm email.

What I see: [no message / message in spam / link opens the wrong site / link 404s / email is not confirmed]
Page: [URL]
Signup request: [status and error code, or "no request to /auth/v1/signup"]
Auth log line, if any: [paste the message]
Address I signed up with: [email]

Fix the confirmation email only. Check these and change only what is wrong:
1. The button calls supabase.auth.signUp for a new account. It does not call signInWithPassword or signInWithOtp instead.
2. options.emailRedirectTo is an absolute https URL on the production origin, on a route that exists. If it is missing, say so. Do not point it at localhost or a preview host. Do not use window.location.origin unless that origin is the published site.
3. Leave a comment listing the Site URL and every Redirect URL I must add under Authentication, then URL Configuration, including that emailRedirectTo value. Do not invent dashboard values.
4. The Confirm signup template still links through {{ .ConfirmationURL }}, or through a route on my site that reads token_hash and type. Do not replace the verification link with the bare Site URL.
5. If the redirect comes back with a code query parameter, that route calls exchangeCodeForSession. If the template uses token_hash and type, that route calls verifyOtp with those values. Do not mix the two. If the link puts access_token in the URL fragment, the client reads that session and detectSessionInUrl is not false.
6. Do not put an SMTP password in client code. Leave a comment that production mail needs custom SMTP under Authentication, then Emails, then SMTP Settings. The built-in sender only mails organization team addresses and is capped.

Do not disable email confirmation to hide a missing message.

How to keep the next edit from doing this

  1. After any prompt that touches signup, auth routes, or email copy, use a private window on the published URL and one new address. Finish the link. Confirm the user row has a confirmation time, then reload.
  2. In the diff, look for signUp, emailRedirectTo, window.location.origin, exchangeCodeForSession, verifyOtp, detectSessionInUrl, ConfirmationURL, SiteURL, TokenHash, and auth/confirm.
  3. In the same prompt as a visual change, tell the editor not to rename the callback path, not to rewrite the confirmation template, and not to drop emailRedirectTo. Keep an auth rewrite out of an unrelated feature so the diff stays small enough to check.
  4. Write down the production origin, the callback path, whether Confirm email is on, and whether custom SMTP is saved. The homepage can stay green while this message stops.

The free scan only loads the page. It does not create a user or open an inbox. To rerun the on-site steps, create an account and describe the flow, such as “Sign up with a new email, then open the dashboard.” Test emails end in @example.com. The watch stays on your hostname and skips email codes. Open one real confirmation yourself when the edit touches the template or the redirect.

Starter is $19/mo. Pro is $49/mo. What each plan includes, and how a watched flow compares with testing by hand, is on the facts page. FlowPatrol is not affiliated with Supabase or Lovable.