Why this happens
Every Supabase auth email (magic link, signup confirmation, password reset) contains a redirect_to target. Supabase picks it like this:
- If your code passed
emailRedirectTo(orredirectTo) and that URL matches an entry in Redirect URLs, it uses that. - Otherwise it falls back to the Site URL.
New projects start with a Site URL of http://localhost:3000. So if your code doesn't pass a redirect, or passes one that isn't allowlisted, production users end up on localhost and see "This site can't be reached".
A second cause is a customized email template that builds the link from {{ .SiteURL }}. That ignores whatever redirect your code asked for.
How to fix it, step by step
1. Set the Site URL. In the Supabase dashboard, go to Authentication → URL Configuration and set Site URL to https://yourdomain.com. On Lovable Cloud this lives under More → Cloud → Users → Auth settings → Advanced.
2. Add your Redirect URLs.
https://yourdomain.com/**
http://localhost:3000/**Keep localhost here, in the allowlist, rather than as the Site URL, so local development still works. On Vercel previews you can add https://*-<team>.vercel.app/**, and on Netlify previews https://**--<site>.netlify.app/**.
3. Pass the redirect from your code.
const { error } = await supabase.auth.signInWithOtp({
email,
options: { emailRedirectTo: `${window.location.origin}/auth/callback` },
});In Next.js server code, where there's no window, use a production env var instead, like the helper in Supabase's docs:
const getURL = () => {
let url = process.env.NEXT_PUBLIC_SITE_URL ?? process.env.NEXT_PUBLIC_VERCEL_URL ?? 'http://localhost:3000/';
url = url.startsWith('http') ? url : `https://${url}`;
return url.endsWith('/') ? url : `${url}/`;
};Set NEXT_PUBLIC_SITE_URL=https://yourdomain.com for the Production environment and redeploy.
4. Check your email template. If you edited the Magic Link or Confirm signup template and it uses {{ .SiteURL }}, either keep the default {{ .ConfirmationURL }}, or switch to {{ .RedirectTo }} if you build your own link:
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Sign in</a>5. Make sure the callback route exists. If you use the PKCE flow (the default with @supabase/ssr), the redirect target needs a route that calls supabase.auth.exchangeCodeForSession(code). A 404 there looks like "login doesn't work".
6. Test with a fresh email. Links that were already sent keep their old target, so always request a new one after changing these settings.
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Claude, ChatGPT, Replit, v0 or whichever tool you build with. Replace the bracketed parts first.
My Supabase magic link and confirmation emails send production users to http://localhost:3000. Fix the redirect handling only:
1. Every call to signInWithOtp, signUp and resetPasswordForEmail must pass emailRedirectTo built from window.location.origin (or NEXT_PUBLIC_SITE_URL on the server), plus our existing callback path.
2. Make sure the callback route exists and exchanges the code for a session.
3. Tell me the exact Site URL and Redirect URLs to set in Supabase for https://yourdomain.com.
Don't change the UI or any unrelated auth logic, and don't disable email confirmation.How to catch this next time
This setting changes quietly. It can happen when you clone a project for staging, move to a new domain, or let an AI tool regenerate your auth helper. The only reliable test is a real email to a real inbox, opened in a private window, after any domain or auth change.
Heedza watches the other side of that flow. It checks on a schedule that your live signup and login pages load and show their email field, and it emails you a screenshot and a fix prompt if they don't. It doesn't send itself magic links, so keep the email test on your release checklist. The free scan shows what Heedza can see on your live URL today.