Why this happens
Google sign-in through Supabase makes three hops: your app → Google → Supabase's callback → your app. Each hop has its own rule:
- Google → Supabase: Google only redirects to URIs listed on your OAuth client. That URI is Supabase's callback,
https://<project-ref>.supabase.co/auth/v1/callback, not your app's URL. A wrong value here givesError 400: redirect_uri_mismatch. - Supabase → your app: Supabase only returns to URLs on its Redirect URLs list. If the URL isn't listed, it falls back to the Site URL, which defaults to
http://localhost:3000. - Your code: if
redirectTois hardcoded to localhost or a preview domain, users go there even when the dashboard settings are right.
People often fix one or two of these, test in a browser that still has an old session, and think it works.
How to fix it, step by step
1. Supabase: Site URL. Go to Authentication → URL Configuration and set it to https://yourdomain.com.
2. Supabase: Redirect URLs. Add https://yourdomain.com/**, plus https://www.yourdomain.com/** if you serve www. If you use a callback route, you can list its exact path instead, e.g. https://yourdomain.com/auth/callback.
3. Google Cloud Console (Google Auth Platform → Clients → your Web client):
- Authorized JavaScript origins:
https://yourdomain.com - Authorized redirect URIs:
https://<project-ref>.supabase.co/auth/v1/callback. Copy it exactly from Authentication → Providers → Google in Supabase. If you've set up a Supabase custom domain, use that host instead.
Remove the http://localhost origins once you're in production, as Google's setup notes for Supabase recommend.
4. Your code. Build redirectTo from where the user actually is:
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${window.location.origin}/auth/callback` },
});With server-side auth (Next.js and @supabase/ssr), the callback route must exchange the code:
// app/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) return NextResponse.redirect(`${origin}/`);
}
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}5. Lovable Cloud users: with Managed by Lovable Google sign-in, Lovable handles the Google side. With your own credentials, add every redirect URI Lovable lists under More → Cloud → Users → Auth settings → Google to your Google client, then select them in Lovable and click Save.
6. Test properly. Use a private window, sign out of any old session first, and test on the live domain, not the preview.
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.
Google sign-in on https://yourdomain.com sends users to localhost (or fails) after deploy. Fix only the OAuth redirect:
1. Change signInWithOAuth so redirectTo is `${window.location.origin}/auth/callback` (or our existing callback path), with no hardcoded localhost or preview URLs.
2. Make sure the callback route exists and exchanges the code for a session, then sends the user to the home page.
3. Give me a checklist of the Supabase Site URL, the Redirect URLs, and the Google Cloud authorized origin and redirect URI I need for this domain.
Don't touch the UI, other providers or the database.How to catch this next time
Google OAuth breaks again when someone adds a domain, rotates the OAuth client, or regenerates the auth code. Write the four settings down next to your deploy notes.
Heedza checks that your live login page loads and shows its sign-in form on a schedule, and it emails a screenshot and a fix prompt when it doesn't. It doesn't sign in through Google for you, so after any domain change also do one private-window Google sign-in. You can see what Heedza checks on your site with the free scan.