Expo + Supabase OAuth Redirect Doctor · free, client-side

Supabase OAuth stuck on localhost in Expo?

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.

Three reasons your OAuth flow is silently broken.

Every Expo + Supabase team hits the same three walls. None of them show up in the docs because they only break when all three configs disagree at the same time.
P01 · redirect

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.

silent fallback
P02 · runtime

Expo Go, dev builds, and standalone all produce different URLs.

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.

runtime drift
P03 · allowlist

Supabase, Expo scheme, and Google/Apple console disagree.

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.

three-way mismatch

Fill in your config. Get the exact mismatches.

Nothing you type is sent anywhere. The check runs in your browser, using the same rules documented by Expo and Supabase.
fn RedirectDoctor.diagnose(config)
client-side · no network
Expo
Supabase
OAuth provider
Your code
Result · idle
# fill in the form and press Diagnose or ⌘↵
status
idle

One function. Config in, diagnosis out.

No server, no SDK key, no signup. doctor.js is plain JavaScript — read it, fork it, or call it directly from your own scripts or CI.
doctor.js 0 dependencies
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.

// free, open, and demand-gated for what comes next.
// found a case it gets wrong? open a GitHub issue.
// no ads, no tracking beyond anonymous usage counts.

Free while in beta.

No account, no payment, no usage limit. If that ever changes, this version stays free for the people already using it.
Free — unlimited diagnoses, no signup, no rate limit.

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.

What people actually search for.

Answers to the questions this tool exists to solve — same rules the checker above runs, in plain language.

Why does Supabase OAuth redirect to localhost in my Expo app?

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.

What redirect URL should I add to Supabase for Expo Go?

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.

Do I need exchangeCodeForSession with Expo and Supabase?

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.

What redirect URI do I add in Google Cloud Console for Supabase OAuth?

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.

Why do I get redirect_uri_mismatch between my Expo dev build and standalone app?

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.

Should skipBrowserRedirect be true on native with Supabase signInWithOAuth?

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.

Stop debugging redirect URLs at 2am.

Paste your config, get the exact mismatch, fix it, ship it — free, and it never leaves your browser.