Article
Supabase login redirects to localhost after deploy: the four settings to fix
Published on
The app is live on your domain. A user signs up, clicks the confirmation link
in the email, and lands on http://localhost:3000 with a "this site can't be
reached" page. Or Google sign-in finishes and bounces them to
localhost:8080, or back to the Lovable preview, or to the lovable.app
address instead of your domain.
Nothing is wrong with the user's browser. Supabase Auth decides where to send
someone after login from four settings, and at least one of them still points
at the place the app was built. Supabase's own default for the Site URL is
http://localhost:3000, which is why that address shows up even in apps that
never ran on port 3000.
The four places, in the order worth checking:
- The Site URL in the auth settings.
- The Redirect URLs allow list next to it.
- The redirect address the code passes when it starts a login.
- The email templates, plus any emails already sent.
Google sign-in adds a fifth, the Google Cloud console, covered at the end.
How Supabase picks the address
When your code starts a login, it can pass a redirect address:
redirectTo for signInWithOAuth and resetPasswordForEmail,
emailRedirectTo for signUp and signInWithOtp. Supabase sends the user
there only if the address matches an entry in the Redirect URLs allow list. If
the code passes nothing, or passes something not on the list, the user goes to
the Site URL.
So "always goes to localhost" almost always means one of two things: the Site URL is still localhost and the code passes nothing, or the code passes a production address that is missing from the allow list and Supabase falls back to a Site URL that is still localhost.
1. Set the Site URL to your real domain
The Site URL is the default destination and the base for links in the auth
emails. It should be your production address with the exact protocol and host
users see: https://www.example.com and https://example.com are different
addresses here.
Where it lives depends on how the project's backend is set up:
- Your own Supabase project (Lovable connected to Supabase, Bolt, v0, or any hand-built app): Supabase dashboard, Authentication, URL Configuration.
- Lovable Cloud: More, then Cloud, then Users, then Auth settings, under
Advanced. Lovable's documentation says it sets the Site URL to your
lovable.appaddress when you publish and adds a custom domain to the Redirect URLs when you connect one. It does not change a Site URL you set yourself. So if users land on thelovable.appaddress instead of your own domain, set the Site URL to the custom domain here.
Apps on their own Supabase project get none of that automation; Lovable's docs say so explicitly. Every address has to be added by hand.
2. Put every real address on the Redirect URLs list
The allow list should contain each address your app sends users back to.
Supabase supports wildcards: * matches anything except . and /, while
** matches anything including slashes. A reasonable list for a typical app:
https://www.example.com/**
https://example.com/**
http://localhost:8080/**
The last line keeps local development working; use your dev server's port
(Lovable's Vite projects use 8080, a plain Vite app 5173, Next.js 3000).
Supabase's guidance is to use ** for local and preview addresses and to be
more exact for production, for example https://www.example.com/auth/callback
if that is your only return path. A broad production wildcard is not a hole by
itself, but an exact path is easier to reason about.
For Vercel preview deployments, Supabase documents a pattern such as
https://*-your-team.vercel.app/**, so previews can log in without you adding
each one.
3. Find the hardcoded localhost in the code
AI tools often write the redirect address as a literal while building in the
preview, and it survives every later change. Search the codebase for
localhost, redirectTo, emailRedirectTo and the preview hostname. A
typical offender:
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'http://localhost:8080/dashboard' },
})
In a browser-only app (Vite, React), build the address from where the user actually is:
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${window.location.origin}/dashboard` },
})
and the same for emailRedirectTo in signUp and redirectTo in
resetPasswordForEmail. In server-rendered apps (Next.js), window is not
available on the server, so read the address from an environment variable
such as NEXT_PUBLIC_SITE_URL and set it per environment. Supabase's docs
suggest exactly that for Vercel.
Whatever the code produces must match an entry from section 2, or Supabase silently falls back to the Site URL and you are back where you started.
4. Fix the email templates, then send new emails
Confirmation, magic link, invite and password reset emails are built from
templates under Authentication, then Emails (Email Templates in older
dashboards). The default templates use {{ .ConfirmationURL }}, which already
includes the redirect address. Custom templates sometimes build the link from
{{ .SiteURL }} instead, which ignores emailRedirectTo entirely. Supabase's
docs say that when you pass a redirect address you may need to replace
{{ .SiteURL }} with {{ .RedirectTo }} in the templates.
Then the step that wastes the most time: emails already sent keep the old link. After changing any of these settings, test with a fresh signup or a new reset request, not with the email from an hour ago.
5. Google sign-in: the Google Cloud side
With your own OAuth client, Google needs to know two addresses, and they are different:
- Authorized redirect URIs: Supabase's callback, not your site. For a
hosted Supabase project it looks like
https://<project-ref>.supabase.co/auth/v1/callback, and the exact value is shown in the Supabase dashboard on the Google provider page. Google redirects to Supabase; Supabase then redirects to your app using sections 1 to 3. - Authorized JavaScript origins: your site's addresses, including the custom domain.
A redirect_uri_mismatch error from Google means the first list is wrong. A
successful Google screen followed by a localhost page means Google is fine and
the problem is in sections 1 to 3. On Lovable Cloud with Lovable-managed
Google sign-in, Lovable handles the Google side; if you switched to your own
credentials, Lovable's docs note that each new domain also needs its redirect
URI added in Google Cloud.
The redirect lands on your domain, then 404s
One last variant: the user is sent to the right domain, for example
https://example.com/auth/callback?code=..., and gets a 404. That is not
Supabase. The app is a single-page app, the static host has no file at
/auth/callback, and nobody configured the host to serve index.html for
every route. On Vercel, a vercel.json with a rewrite does it:
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }
Netlify uses a _redirects file with /* /index.html 200. If the page loads
but the user is still logged out, check that the client is created with the
default detectSessionInUrl, which reads the code from the address and
finishes the login.
Test it like a user
Open a private window on a device that has never seen the preview, sign up
with a new address, click the email, log out, log in with Google, reset the
password. Each flow should end on your domain with the user logged in. Then do
it once on www and once without, because a session created on one host is
not visible on the other.
If all of that passes and logged-in users then hit permission errors on their first save, the next stop is the "new row violates row-level security policy" error, which often turns out to be a missing session.
Login redirects are one of six things that usually break between preview and production. If you have fixed this and two other things broke, the app probably needs someone to read it end to end. That is our vibe code rescue audit: 450 EUR, 48 working hours, a written report of what is broken and what each fix costs, with the fee deducted if you continue into the rescue (1,500-6,000 EUR, fixed scope).