# Supabase OAuth in Expo: Why Sign-In Never Comes Back to Your App

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:

1. Your app asks Supabase for an OAuth URL, saying "send the user back to `myapp://...` when done."
2. The app opens that URL in an in-app browser session.
3. The user signs in with Google, Apple or GitHub.
4. Supabase redirects to `myapp://...` with the session tokens attached.
5. The OS sees the `myapp://` scheme, closes the browser session and hands the URL back to your app.
6. 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](https://supabase.com/docs/guides/auth/native-mobile-deep-linking?platform=react-native) 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

```json
{
  "expo": {
    "scheme": "myapp"
  }
}
```

The scheme is baked into the native app. Expo's [AuthSession docs](https://docs.expo.dev/versions/latest/sdk/auth-session/) 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:

```bash
npx expo install expo-auth-session expo-web-browser expo-linking
```

Then the sign-in function:

```tsx
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: true`** stops 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 to `redirectTo`. 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](https://supabase.com/docs/guides/auth/redirect-urls) 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 sends `myapp://`, and that isn't allowlisted.
- You changed the `scheme` but 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

1. **Log the redirect.** `console.log(makeRedirectUri())` on the device you're testing. That exact string is what Supabase must allow.
2. **Compare it with the allowlist** in Authentication > URL Configuration. Does a pattern there match it, including the path?
3. **Check the scheme** in `app.json` matches the start of that string, and that you rebuilt after changing it.
4. **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.
5. **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](https://www.applighter.com/?utm_source=hashnode&utm_medium=blog&utm_campaign=expo-supabase-oauth-redirect), 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?

