HeHeedza

Why does my Supabase magic link redirect to localhost:3000 in production?

Last reviewed

Short answer

Your Supabase Site URL is still http://localhost:3000, which is the default, and the email link falls back to it. Set the Site URL to your live domain, add your live URLs to Redirect URLs, pass emailRedirectTo from your code, then send yourself a new link.

Why this happens

Every Supabase auth email (magic link, signup confirmation, password reset) contains a redirect_to target. Supabase picks it like this:

  1. If your code passed emailRedirectTo (or redirectTo) and that URL matches an entry in Redirect URLs, it uses that.
  2. 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.

Fix prompt
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.

Questions

I set the Site URL but links still go to localhost. Why?

Either you're clicking an email sent before the change, or your code passes a redirect that isn't on the allowlist, or your template uses {{ .SiteURL }} from a stale project. Send a fresh link, then check the redirect_to value in the email's link.

Should localhost stay in Redirect URLs?

It's fine there for local development. Just don't leave it as the Site URL, which is the fallback for everyone.

Can I use wildcards for preview deployments?

Yes. Supabase supports * and ** patterns, and its docs give examples for Vercel and Netlify previews. For production, Supabase recommends exact paths.

Sources