Supabase login redirects to localhost or your preview URL after publishing
This one shows up constantly from people who just published a Lovable, Bolt, Replit or v0 app. Login works fine in the preview. Then on the real domain, Google sign-in bounces back to http://localhost:3000, confirmation emails open the old preview link, or users get stuck in a login loop.
The app code is usually fine. Supabase decides where to send people after login, and it only sends them to addresses you’ve told it about. Out of the box, the fallback address is http://localhost:3000.
This guide assumes your app talks to your own Supabase project. If you’re on a builder’s built-in backend (for example Lovable Cloud) rather than a Supabase project you can open yourself, look for the same Site URL and redirect settings in that builder’s auth settings.
Why it happens
There are two redirect hops in a typical login, and either one can still point at the old address:
- Provider to Supabase. Google, GitHub and other OAuth providers send the user back to your Supabase project’s callback URL (
https://<project-ref>.supabase.co/auth/v1/callback). That’s configured at the provider. - Supabase to your app. Supabase then sends the user to the
redirectToaddress your code asked for, but only if that address is on the project’s Redirect URLs allow list. If it isn’t, Supabase quietly falls back to the Site URL.
So when the Site URL is still http://localhost:3000 (the default) or your preview link, and your live domain was never added to the allow list, every login ends up there.
1. Set the Site URL to your live domain
In the Supabase dashboard, go to Authentication → URL Configuration and set Site URL to the address real users visit:
https://yourapp.com
No trailing path. This is also the default destination used in emails like signup confirmation, so getting it right fixes a lot of “the link opens the preview” reports on its own.
2. Add every address you use to Redirect URLs
On the same page, add each place users should be able to land after login. Exact URLs are the most predictable:
https://yourapp.com/**
https://www.yourapp.com/**
http://localhost:5173/**
Add your preview domain too if you still test there. Supabase allows wildcards (* matches within one part of the address, ** matches across slashes), which is handy for preview deploys that get a new subdomain each time. For your production domain, prefer the most specific pattern that works, such as https://yourapp.com/auth/callback, so the allow list can’t be used to send users somewhere you didn’t intend.
3. Stop hard-coding the redirect in your code
AI-generated auth code often bakes in whatever address the app had when the code was written. Search the project for it:
grep -rnE "localhost|lovable\.app|lovableproject\.com|bolt\.new|replit\.dev|vercel\.app" src/
Then build the redirect from wherever the app is running right now:
// OAuth (Google, GitHub, ...)
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${window.location.origin}/auth/callback` },
});
// Email + password signup confirmation
await supabase.auth.signUp({
email,
password,
options: { emailRedirectTo: `${window.location.origin}/auth/callback` },
});
// Magic link
await supabase.auth.signInWithOtp({
email,
options: { emailRedirectTo: `${window.location.origin}/auth/callback` },
});
// Password reset
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/reset-password`,
});
Whatever path you use here (/auth/callback, /reset-password) has to exist as a real route in your app, and it has to match an entry in Redirect URLs.
If your app reads the site address from an environment variable such as VITE_SITE_URL, remember that Vite bakes it in at build time. Changing it in your host’s settings does nothing until you rebuild and redeploy.
4. Check the OAuth provider’s callback
If Google shows Error 400: redirect_uri_mismatch, the problem is the first hop, not Supabase’s allow list.
- Google: in Google Cloud Console, open APIs & Services → Credentials, edit your OAuth client, and make sure Authorized redirect URIs contains your Supabase callback, exactly as shown on the Google provider page in Supabase (Authentication → Providers → Google). Add your live domain under Authorized JavaScript origins.
- GitHub: in your OAuth app settings, Authorization callback URL should be the same Supabase callback URL.
The provider never needs your app’s URL as the callback. It sends users to Supabase, and Supabase sends them to you.
If you moved Supabase to a custom auth domain, the callback changes to that domain, so update it at the provider as well.
5. Check your email templates
Under Authentication → Emails (templates), confirmation, magic link, invite and reset-password emails should use Supabase’s template variables, such as {{ .ConfirmationURL }}, rather than a pasted link. A template someone customized during the demo phase may still contain the preview address in plain text.
6. Test it like a real user
On the live domain, in a private browser window:
| Flow | What should happen |
|---|---|
| Sign up with email | Confirmation email link opens your live domain and you’re logged in |
| Log in with each OAuth provider | You land back on the live domain, not localhost or preview |
| Magic link (if used) | Opens the live domain and logs you in (open it in the browser you requested it from) |
| Reset password | Reset link opens your live /reset-password page |
| Log out, log back in | No loop back to the login screen |
Common gotchas
- The change didn’t seem to work. Old emails keep their old links. Request a new confirmation or magic link after changing settings.
- Magic link fails when opened on another device. If your app uses the PKCE sign-in flow (the default in Supabase’s server-side helper,
@supabase/ssr), the link has to be opened in the same browser that requested it. Requesting it on your laptop and opening it on your phone can fail even with perfect redirect settings. - www vs no www.
https://yourapp.comandhttps://www.yourapp.comare different addresses to Supabase. Allow both, or redirect one to the other at your host. - Login loop on the live domain only. Usually the redirect landed on a route that doesn’t finish the sign-in (no
/auth/callbackhandler), or the app checks for a session before the client has processed the login. Confirm the callback route exists in the deployed build. - Mobile apps (Expo, Capacitor). The redirect is your app’s custom scheme (for example
myapp://auth/callback), and that scheme must be in Redirect URLs too.
Quick checklist
| Check | Where |
|---|---|
| Site URL is the live domain | Supabase → Authentication → URL Configuration |
| Live, www, preview and localhost addresses allowed | Same page, Redirect URLs |
| No hard-coded localhost or preview links | grep your src/ folder |
Provider callback is the Supabase /auth/v1/callback URL | Google Cloud Console / GitHub OAuth app |
| Email templates use variables, not pasted links | Supabase → Authentication → Emails |
| Every flow tested on the live domain | Private window |
Not sure what your published app is exposing while you’re in there? The free app scanner checks a live URL for secret-looking keys in its JavaScript. And if login is one of several things broken after publishing, a flat-fee Rescue fixes auth along with the rest and proves it with a before/after verification report.