OAuth redirects to localhost even in production.
Supabase falls back to its Site URL when redirectTo is missing or rejected — and that Site URL still says http://localhost:3000 because nobody changed it.
Paste your Expo + Supabase 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 when redirectTo is missing or rejected — and that Site URL still says http://localhost:3000 because nobody changed it.
exp://127.0.0.1:8081/--/... in Expo Go, myapp://... in a dev build, a custom scheme in TestFlight. One config never covers all three.
The provider console wants the Supabase callback. Supabase wants your app scheme on its allow-list. Expo wants its dev URL. Miss one and the redirect dies — usually after the user already typed their password.
import { diagnose } from './doctor.js'; // or, loaded globally: const { diagnose } = window.RedirectDoctor; const result = diagnose({ expo: { scheme, path, runtime, lanIp, port }, supabase: { projectUrl, siteUrl, allowedRedirectUrls }, provider: { name, authorizedRedirectUris }, code: { redirectTo, flowType, skipBrowserRedirect, ... }, }); // result { "status": "fail" | "warn" | "pass", "summary": "…", "expectedRedirects": { expoGo, devBuild, standalone, web, supabaseCallback }, "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 redirect URL each Expo runtime actually produces against your app.json scheme, your Supabase redirect allow-list (glob-matched the same way Supabase matches it), 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 Expo, Supabase, Google, or Apple.
Supabase falls back to your project's Site URL whenever redirectTo is missing, empty, or not covered by the redirect allow-list. If Site URL is still set to something like http://localhost:3000 — a common default nobody changes after setup — a rejected redirect sends the user there silently, including in production. Fix it by setting Site URL to your real production URL and making sure redirectTo is on the allow-list.
Expo Go doesn't use your app's custom scheme — it uses exp://127.0.0.1:8081/--/<path> (or your machine's LAN IP instead of 127.0.0.1 when testing on a physical device). Add that exact exp:// URL, or a pattern that covers it, to Supabase's redirect allow-list separately from the scheme://** entry you add for dev/standalone builds.
Yes, if you're using flowType: 'pkce', which is the recommended flow for native apps. The redirect callback carries a code query parameter, not a session — you must call supabase.auth.exchangeCodeForSession(url) in your deep-link handler to turn that code into an actual session.
Add the exact Supabase auth callback URL: https://<your-project-ref>.supabase.co/auth/v1/callback. OAuth providers require an exact string match here — wildcards aren't accepted — so this same URL, not your app's custom scheme, is what goes in Google Cloud Console, the Apple Services ID's Return URLs, or a GitHub OAuth App's Authorization callback URL.
Dev builds and standalone builds both resolve to the same scheme://path formula — there's no separate "standalone" URL shape. So a mismatch here almost always means the scheme registered in app.json doesn't match what's on the Supabase allow-list or what redirectTo actually builds, not a real difference between the two build types.
Yes. On native, skipBrowserRedirect: true stops Supabase from trying to issue its own browser redirect; instead you take the returned URL and open it yourself (for example with expo-web-browser), which is what lets the OAuth flow return correctly to your app's custom scheme.
Paste your config, get the exact mismatch, fix it, ship it — free, and it never leaves your browser.