Notes
Supabase OAuth redirects to localhost in production: the allow-list rule nobody reads
You see this after a deploy, not before: a user clicks "Continue with Google" on your live domain, gets through the Google consent screen, and the browser lands them back on http://localhost:3000/?code=4/0AeaYSHD... instead of your production site. The login itself worked. Supabase just sent the browser somewhere else afterward, and it did that on purpose: whenever the redirect it would use isn't covered by the Redirect URLs allow-list, Supabase falls back to the Site URL in your project's Authentication settings, silently, with no error page and no console warning. If that Site URL is still set to localhost, so is every failed redirect, including ones in production.
1. Site URL is still Supabase's own default
A new Supabase project ships with Site URL set to http://localhost:3000. That value isn't just a placeholder: per Supabase's own docs, "the Site URL in URL Configuration defines the default redirect URL when no redirectTo is specified in the code," and it's also the fallback for any redirectTo that gets rejected. Plenty of projects go to production without anyone ever opening this setting, so the fallback stays localhost forever.
Site URL (still the default): http://localhost:3000
Site URL (fix): https://yourapp.com
Fix: in the Supabase dashboard, go to Authentication → URL Configuration → Site URL and set it to your exact production origin, https, no trailing slash.
2. The callback URL isn't on the Redirect URLs allow-list
Even with a correct Site URL, an explicit redirectTo that isn't on the allow-list gets rejected the same way. The allow-list uses glob patterns, not plain strings: Supabase's docs define * as matching "any sequence of non-separator characters," ** as "any sequence of characters," and state that "the separator characters in a URL are defined as . and /." A pattern that looks close enough on a quick read often isn't a match at all, and a trailing-slash difference between your callback and the allow-list entry is enough to fail.
Redirect URLs
https://yourapp.com/auth/callback
https://yourapp.com/**
Fix: add the exact callback URL your app builds, then a broader https://yourapp.com/** pattern to cover any path under it. If you deploy previews on Vercel or Netlify, add their documented wildcard too: https://*-<team-slug>.vercel.app/** or https://**--<site-name>.netlify.app/**, or preview builds will fail the same way production did.
3. redirectTo is hardcoded to localhost
The most direct cause: a literal localhost string sitting in the signInWithOAuth call, written during local development and never swapped out.
// wrong: ships to production verbatim
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'http://localhost:3000/auth/callback' }
})
// fix: derive it from where the app is actually running
await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: `${window.location.origin}/auth/callback` }
})
On a server-rendered route, guard the origin instead of hardcoding either value: typeof window !== 'undefined' ? window.location.origin : process.env.NEXT_PUBLIC_SITE_URL.
4. process?.env quietly resolves to undefined
A subtler version of the same bug, reported in a Supabase GitHub discussion: a Next.js app read its production URL from process?.env.NEXT_PUBLIC_SITE_URL, worked in local dev, and redirected to localhost once deployed. The cause is how Next.js inlines env vars: as one commenter put it, "Next.js inlines process.env.NEXT_PUBLIC_* via static analysis at build time. It looks for that literal member-expression pattern in your source and swaps in the real string. process?.env?.NEXT_PUBLIC_SITE_URL (or any dynamic/bracket access) doesn't match that pattern, so the bundler can't inline it." The optional chaining on process itself, not on a property, is what breaks the build-time substitution: the browser bundle keeps the literal expression, `process` is undefined client-side, and the whole thing silently becomes `undefined`.
// wrong: optional chaining on process defeats build-time inlining
const base = process?.env.NEXT_PUBLIC_SITE_URL
// fix: direct property access
const base = process.env.NEXT_PUBLIC_SITE_URL
Fix: search your codebase for process?.env and replace it with plain process.env.VAR, or import.meta.env.VAR on Vite.
5. Site URL doesn't match your production domain
A Site URL that's set, uses https, and still isn't localhost can still be wrong: a www vs. apex mismatch, or a different domain than the one users actually land on. Supabase treats Site URL as an exact match against your app's real origin, so https://example.com and https://www.example.com are two different values to it, not the same site.
Site URL: https://example.com
Where users land: https://www.example.com ← mismatch
Fix: pick one as canonical, set Site URL to exactly that, and if you still serve the other domain, add it to the allow-list too or redirect it to the canonical one before it ever reaches Supabase.
6. Checklist
- Set Supabase Site URL to your exact production origin: https, no trailing slash, no www/apex mismatch.
- Add your app's exact callback URL to Authentication → URL Configuration → Redirect URLs.
- Search the codebase for a hardcoded
localhostin anyredirectTooremailRedirectTocall. - Search for
process?.envand switch it to direct property access. - If you deploy previews on Vercel or Netlify, allow-list their wildcard pattern too.
- Re-check after every deploy: Supabase settings and your OAuth provider console drift independently from your code.
7. Check it in 30 seconds
Paste your production origin, Supabase Site URL and Redirect URLs into the Supabase Auth Redirect Doctor and it lists every mismatch by name, with the exact value to fix it.
8. Sources
- Supabase docs: Redirect URLs. Site URL as default redirect, and the glob syntax for the allow-list (
*,**, and.//as separators). - Supabase docs: Login with Google. The
https://<project-ref>.supabase.co/auth/v1/callbackformat and where to add it in Google Cloud Console's Authorized redirect URIs. - Supabase docs: Server-side rendering with Next.js.
@supabase/ssr,createServerClient/createBrowserClient, and cookie-based sessions. - Supabase GitHub discussion #38063. The reported
process?.envcase and why optional chaining onprocessdefeats Next.js's build-time env inlining.