OAuth redirects to localhost even in production.
Supabase falls back to its Site URL whenever redirectTo is missing or not on the allow-list — and Site URL still says http://localhost:3000 because nobody changed it after the project was created.
Paste your Next.js, Vite, or SvelteKit + Supabase Auth redirect config and get exact mismatch fixes before wasting another afternoon.
Runs entirely in your browser. No account, no backend, no payment.
Supabase falls back to its Site URL whenever redirectTo is missing or not on the allow-list — and Site URL still says http://localhost:3000 because nobody changed it after the project was created.
The Redirect URLs allow-list matches by glob, not by domain equivalence — an entry for www.example.com does not cover the apex domain, a trailing slash breaks an exact match, and Vercel/Netlify preview URLs need their own pattern.
PKCE needs a server route that calls exchangeCodeForSession() — miss it and the flow dies with a code in the URL nobody reads. And Google/GitHub want the Supabase callback URL, not yours.
import { diagnose } from './doctor-web.js'; // or, loaded globally: const { diagnose } = window.RedirectDoctorWeb; const result = diagnose({ app: { productionOrigin, callbackPath, framework, usesSsrPackage, flowType, envSiteUrlVar, deployedOn }, supabase: { projectUrl, siteUrl, allowedRedirectUrls }, provider: { name, authorizedRedirectUris }, code: { redirectToSnippet, callbackRouteExists, callsExchangeCodeForSession }, }); // result { "status": "fail" | "warn" | "pass", "summary": "…", "expected": { siteUrl, callbackUrl, allowListEntries, supabaseCallback, previewPattern }, "problems": [ { severity, code, message, where } ], "fixes": [ { title, value, where } ], "checklist": [ "…" ], "disclaimer": "…" }
Everything runs client-side. The form above calls this exact function in your browser. There is no backend, no API key, and no request that carries your config anywhere.
It checks the callback URL your app will actually use against your Supabase redirect allow-list (glob-matched the same way Supabase matches it), your Site URL, and the exact callback your OAuth provider console expects.
This started as a tool I needed myself. If it saves you an afternoon, the best way to say thanks is to tell me what it got wrong — open a GitHub issue.
Read-only, client-side analysis. Nothing you enter is sent anywhere — the check runs entirely in your browser against the values you typed, not your live project. Always verify in your own environment. Not affiliated with Supabase, Vercel, Netlify, Google, GitHub, Microsoft, or Discord.
Supabase falls back to your project's Site URL whenever the redirectTo value your code sends doesn't exactly match an entry on the Redirect URLs allow-list. If Site URL was never changed from its default of http://localhost:3000, any request that fails that allow-list check — a missing entry, a trailing slash, http vs https — lands back on localhost even though the app is live on Vercel.
Site URL should be your app's canonical production origin — e.g. https://app.example.com — with no trailing slash and no path. It is the fallback Supabase uses whenever redirectTo doesn't match the allow-list, so leaving it at the default localhost value after deploying is what causes production sign-in to bounce back to localhost.
Add a wildcard allow-list entry such as https://*.vercel.app/** alongside your production URL, since every preview deployment gets its own subdomain that a single hardcoded entry won't match. Without a wildcard pattern, OAuth on preview branches will keep falling back to Site URL even though production works fine.
Yes, if your app uses the PKCE flow, which is the default for @supabase/ssr. The provider redirects back with a ?code= query parameter, and your callback route must call exchangeCodeForSession(code) server-side to exchange it for a session — without that call the code sits unused in the URL and the user is never actually signed in.
Google Cloud Console — and any other OAuth provider console — needs Supabase's own callback URL, https://<your-project-ref>.supabase.co/auth/v1/callback, not your app's domain or your /auth/callback route. Supabase is the OAuth client that talks to Google directly; your app's callback route only ever receives the redirect from Supabase afterward.
Next.js inlines NEXT_PUBLIC_* variables at build time by statically matching the literal process.env.NEXT_PUBLIC_SITE_URL expression in your source code. Optional chaining, as in process?.env?.NEXT_PUBLIC_SITE_URL, doesn't match that pattern, so the bundler can't inline it — in the browser bundle it silently evaluates to undefined instead of your URL, sending redirectTo down the Site URL fallback path instead of your real domain.
Paste your config, get the exact mismatch, fix it, ship it — free, and it never leaves your browser.