Supabase OAuth in Expo: Why Sign-In Never Comes Back to Your App
The app scheme, makeRedirectUri() and Supabase's Redirect URLs have to agree on one URL. Here's how to make them match.

You add "Sign in with Google" to your Expo app. The browser opens, you pick your account, consent... and then nothing. The browser sits on a blank page, or a localhost:3000 page, or it closes and your app is still on the login screen.
The Supabase call is almost never the problem. The problem is that three settings have to agree on one URL, and they usually don't.
This guide shows how OAuth redirects work in an Expo + Supabase app, the code that handles them, and how to debug the "never comes back" bug.
How does OAuth get back into a mobile app?
On the web, OAuth ends with a redirect to a URL on your site. A mobile app doesn't have a site, so it uses a custom URL scheme instead, such as myapp://. The flow looks like this:
- Your app asks Supabase for an OAuth URL, saying "send the user back to
myapp://...when done." - The app opens that URL in an in-app browser session.
- The user signs in with Google, Apple or GitHub.
- Supabase redirects to
myapp://...with the session tokens attached. - The OS sees the
myapp://scheme, closes the browser session and hands the URL back to your app. - Your app reads the tokens from the URL and creates a Supabase session.
If any step points at a different URL than the others, step 5 never happens.
The three settings that must match
| Setting | Where | Example |
|---|---|---|
| App scheme | scheme in app.json / app.config.ts |
"scheme": "myapp" |
| Redirect URL the app sends | makeRedirectUri() from expo-auth-session |
myapp:// in a development build |
| Allowed redirect URLs | Supabase dashboard: Authentication > URL Configuration | myapp://** |
Supabase's native mobile deep linking guide uses exactly this setup: a scheme in the app config, makeRedirectUri() for the redirect, and a scheme://** entry in the redirect allowlist.
Step 1: Set the scheme
{
"expo": {
"scheme": "myapp"
}
}
The scheme is baked into the native app. Expo's AuthSession docs point out that changing it needs a rebuild; an OTA update can't change it. Without a scheme, the auth flow completes but can't pass data back, so the user has to close the browser by hand and your app sees a cancel.
Step 2: Sign in and catch the redirect
Install the libraries:
npx expo install expo-auth-session expo-web-browser expo-linking
Then the sign-in function:
import { makeRedirectUri } from 'expo-auth-session';
import * as QueryParams from 'expo-auth-session/build/QueryParams';
import * as WebBrowser from 'expo-web-browser';
import { supabase } from '@/lib/supabase';
const redirectTo = makeRedirectUri();
async function createSessionFromUrl(url: string) {
const { params, errorCode } = QueryParams.getQueryParams(url);
if (errorCode) throw new Error(errorCode);
const { access_token, refresh_token } = params;
if (!access_token) return;
const { error } = await supabase.auth.setSession({ access_token, refresh_token });
if (error) throw error;
}
export async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo,
skipBrowserRedirect: true,
},
});
if (error) throw error;
const res = await WebBrowser.openAuthSessionAsync(data?.url ?? '', redirectTo);
if (res.type === 'success') {
await createSessionFromUrl(res.url);
}
}
Two details matter here:
skipBrowserRedirect: truestops supabase-js from trying to navigate a browser window that doesn't exist in React Native, and returns the URL so you can open it yourself.openAuthSessionAsync(url, redirectTo)watches for a redirect toredirectTo. When it sees one, it closes the browser and returns the full URL with the tokens. If the redirect goes anywhere else, it never fires.
Supabase's guide also handles links that arrive while the app is opening, with Linking.useLinkingURL() from expo-linking, passing any URL to the same createSessionFromUrl. Add that if you use magic links too.
Step 3: Allow the redirect in Supabase
In the Supabase dashboard, go to Authentication > URL Configuration and add your app's redirect URL under Redirect URLs.
Supabase's redirect URL docs explain the wildcards:
*matches characters within one path segment**matches anything, including/
So myapp://** covers myapp://, myapp://auth/callback and any other path. For production, Supabase recommends exact URLs instead of broad wildcards where you can.
Also check your Site URL. It is the default redirect when no redirectTo is sent, so if the app's redirect never makes it to Supabase, you'll often find users landing on the Site URL (frequently http://localhost:3000 in new projects).
Why does it work in one environment and not another?
makeRedirectUri() returns different values depending on where the app runs. From Expo's docs:
| Environment | Example return value |
|---|---|
| Development build | myapp://redirect |
| Expo Go | exp://127.0.0.1:8081/--/redirect |
| Web (dev) | https://localhost:19006/redirect |
That explains the most common "works for me, broken for you" reports:
- You tested in Expo Go, where the redirect is an
exp://URL with your machine's IP. A teammate's IP is different, so their redirect isn't in the allowlist. - You allowlisted the
exp://URL, then made a development build, which sendsmyapp://, and that isn't allowlisted. - You changed the
schemebut didn't rebuild, so the installed app still registers the old one.
Supabase's own guide targets development builds and standalone apps for this flow, so test OAuth there, not in Expo Go.
A 2-minute debug checklist
- Log the redirect.
console.log(makeRedirectUri())on the device you're testing. That exact string is what Supabase must allow. - Compare it with the allowlist in Authentication > URL Configuration. Does a pattern there match it, including the path?
- Check the scheme in
app.jsonmatches the start of that string, and that you rebuilt after changing it. - Check the provider's own settings. Google, Apple and GitHub must redirect to your Supabase callback URL (
https://<project-ref>.supabase.co/auth/v1/callback), not to your app. - Look at the URL the browser stopped on. A Site URL means the redirect wasn't accepted; an error query string tells you which step failed.
Want this pre-wired?
I work on AppLighter, which sells Expo SDK 54 templates (TypeScript, Expo Router, NativeWind, Supabase with RLS on every table), so the auth and backend groundwork is in place before you add providers like this. The steps above work the same in any Expo project.
Wrapping up
OAuth in an Expo + Supabase app comes down to one URL that three places must agree on: the app's scheme, the redirectTo from makeRedirectUri(), and Supabase's Redirect URLs. Log the redirect, match it in the allowlist, rebuild after scheme changes, and test in a development build.
Which provider gave you the most trouble in Expo: Google, Apple or GitHub?
Comments
Join the discussionNo comments yet. Be the first to comment.
Shipping Secure Expo + Supabase Apps
Practical guides for building React Native apps on Expo and Supabase that are secure from day one: Row Level Security, auth, secrets, and reviewing AI-generated code before it ships.

