Supabase login redirects to localhost after deploy
Google sign-in, magic links, or confirmation emails send users to localhost or back to the login page. How to fix Supabase auth redirect URLs in a Lovable, Bolt, or Cursor app.
What you are seeing
- After Google or GitHub sign-in, the browser lands on
http://localhost:3000and shows "This site can't be reached". - Confirmation and password-reset emails link to localhost instead of your live domain.
- Sign-in works on the published site but not in the editor preview, or the other way round.
- Sign-in seems to succeed, then you land back on the login page still signed out.
- Signup emails stop arriving and the API returns "email rate limit exceeded".
Why it happens
- The Site URL in Supabase is still the default
http://localhost:3000. Supabase falls back to it whenever no redirect is passed or the one passed is not allowed. - The URL you redirect to is not in the Redirect URLs allow list, so Supabase ignores it and uses the Site URL.
- The AI hardcoded
redirectTo: 'http://localhost:3000'while building locally. - The preview and production domains differ and only one of them is allowed.
- With the PKCE flow (Next.js with
@supabase/ssr), the callback route never exchanges thecodefor a session, so the user looks signed out. - Supabase's built-in email sender is for testing only and is limited to 2 emails per hour (per Supabase's docs), so real signups hit the limit fast.
How to fix it
1. Open the auth URL settings
In the Supabase dashboard, open Authentication, then URL Configuration.
2. Set the Site URL and allow every domain you use
Site URL: https://yourapp.com
Redirect URLs: https://yourapp.com/**
https://*-yourteam.vercel.app/**
http://localhost:3000/**
3. Find hardcoded localhost redirects
grep -rnE "localhost:(3000|5173|8080)" src app lib 2>/dev/null
4. Redirect to the current origin, not a fixed URL
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${window.location.origin}/auth/callback` },
});
await supabase.auth.signUp({
email,
password,
options: { emailRedirectTo: `${window.location.origin}/auth/callback` },
});
5. Exchange the code for a session (Next.js App Router)
// app/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
export async function GET(request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
const next = searchParams.get('next') ?? '/';
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) return NextResponse.redirect(`${origin}${next}`);
}
return NextResponse.redirect(`${origin}/login?error=auth`);
}
6. Check the Google side
In Google Cloud Console, the OAuth client's authorized redirect URI must be your Supabase callback, https://<project-ref>.supabase.co/auth/v1/callback, not your app's URL. Your app's own URLs belong in Supabase's Redirect URLs list.
7. Use your own email sender before launch
Add a custom SMTP provider in Supabase's Authentication settings so confirmation and reset emails are not capped by the built-in sender.
How to know it is fixed
Open the live site in a private window, sign in with Google, and confirm you land on your own domain signed in. Then request a password reset and check that the link in the email points at your domain, not localhost.