Skip to main content

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.

Updated 6 min readMarkdown
Part of seriesShipping Secure Expo + Supabase Apps
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 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: 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 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, 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?

No comments yet. Be the first to comment.

Shipping Secure Expo + Supabase Apps

Part 2 of 2

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.

Start from the beginning7 Supabase RLS Mistakes That Leak Data in Expo Apps (and the SQL to Fix Each One)A practical Row Level Security audit for React Native apps that talk to Supabase directly