ReadyNative

2 Oct 2026 · 9 min read

Supabase auth in Expo: email, Apple and Google

Supabase sign-in for an Expo Router app: email with confirmation, native Sign in with Apple, Google via the browser, password reset and account deletion.

Supabase gives an Expo app email/password, Sign in with Apple and Google sign-in, plus a Postgres database with row-level security behind the same user. This guide wires all three sign-in methods in an Expo Router app, and covers the parts the quickstarts skip: redirect URLs, the password reset round trip, and deleting an account, which both stores require.

Good news first: all of it runs in Expo Go. Sign in with Apple works in Expo Go on iOS, and Google opens the system browser.

1. Install and create the client

npx expo install @supabase/supabase-js expo-apple-authentication expo-auth-session expo-crypto expo-web-browser
npx expo install @react-native-async-storage/async-storage   # or any storage adapter

Copy the Project URL and the anon (publishable) key from Project Settings → API into .env as EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY. The anon key is meant to be public; row-level security is what protects your data.

import AsyncStorage from "@react-native-async-storage/async-storage";
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      storage: AsyncStorage,
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false, // there is no browser URL bar: handle deep links yourself
    },
  },
);

Refresh tokens only while the app is in the foreground, as Supabase recommends for React Native:

import { AppState } from "react-native";

AppState.addEventListener("change", (state) => {
  if (state === "active") supabase.auth.startAutoRefresh();
  else supabase.auth.stopAutoRefresh();
});

2. Add your redirect URLs

This is the step most broken setups miss. In Supabase, open Authentication → URL Configuration → Redirect URLs and add your app's scheme, for example myapp:// and myapp://new-password for the reset screen. Without it, Google sign-in and password reset links never come back to the app. In Expo Go the links start with exp://<lan-ip>:8081/--/ instead, so add exp://** to your development project only.

3. Email and password

await supabase.auth.signUp({ email, password });           // sends the confirmation mail
await supabase.auth.signInWithPassword({ email, password });

Leave Confirm email on (the default). signUp then returns no session, so show "check your email" and let the user sign in after clicking the link.

4. Sign in with Apple, natively

Use the native sheet with signInWithIdToken - no Services ID or web redirect needed. In Supabase, enable the Apple provider and put your iOS bundle id under Client IDs; in the Apple Developer portal, turn on the Sign in with Apple capability for that app id. The nonce is the part people get wrong: Apple receives its SHA-256 hash, Supabase the raw value.

import * as AppleAuthentication from "expo-apple-authentication";
import * as Crypto from "expo-crypto";

const rawNonce = Crypto.randomUUID();
const hashedNonce = await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, rawNonce);
const credential = await AppleAuthentication.signInAsync({
  requestedScopes: [
    AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
    AppleAuthentication.AppleAuthenticationScope.EMAIL,
  ],
  nonce: hashedNonce,
});
const { error } = await supabase.auth.signInWithIdToken({
  provider: "apple",
  token: credential.identityToken!,
  nonce: rawNonce,
});

Apple sends the user's name only on the very first sign-in. Save it then (for example with supabase.auth.updateUser) or it is gone. And if you offer Google sign-in on iOS, App Review guideline 4.8 asks for an equivalent privacy-focused option next to it - Sign in with Apple is the usual answer.

5. Google, through the system browser

Enable the Google provider in Supabase with a Web OAuth client from Google Cloud Console, and paste Supabase's callback URL into that client's authorized redirect URIs. The app itself never needs a Google client id:

import { makeRedirectUri } from "expo-auth-session";
import * as WebBrowser from "expo-web-browser";

const redirectTo = makeRedirectUri({ scheme: "myapp" });
const { data } = await supabase.auth.signInWithOAuth({
  provider: "google",
  options: { redirectTo, skipBrowserRedirect: true },
});
const result = await WebBrowser.openAuthSessionAsync(data.url!, redirectTo);
if (result.type === "success") {
  // Implicit flow: tokens arrive in the URL fragment. (A PKCE "code" goes to exchangeCodeForSession.)
  const params = new URLSearchParams(result.url.split("#")[1]);
  await supabase.auth.setSession({
    access_token: params.get("access_token")!,
    refresh_token: params.get("refresh_token")!,
  });
}

6. Protect routes with Expo Router

Keep the sign-in screens in an (auth) group, listen to supabase.auth.onAuthStateChange, and redirect: signed-out users to /(auth)/sign-in, signed-in users out of (auth). One exception to remember: the password-reset screen opens with a fresh recovery session, so do not bounce it to Home the moment that session starts.

7. Password reset is a round trip

  1. The user enters their email; call supabase.auth.resetPasswordForEmail(email, { redirectTo: "myapp://new-password" }).
  2. The link in the mail opens your new-password screen with tokens in the URL.
  3. Activate that recovery session with setSession (or exchangeCodeForSession for PKCE).
  4. Save the new password with supabase.auth.updateUser({ password }).
  5. Show a clear "link expired" state - used links stop working.

8. Account deletion (required by both stores)

App Store guideline 5.1.1(v) and Google Play both require in-app account deletion. The anon key cannot delete auth users, so add a security definer function, callable only by the signed-in user:

create function public.delete_user() returns void
language sql security definer set search_path = ''
as $$ delete from auth.users where id = auth.uid(); $$;

revoke execute on function public.delete_user() from public, anon;
grant execute on function public.delete_user() to authenticated;

Call it with supabase.rpc("delete_user"), then sign out locally. Two catches: your own tables should reference auth.users with on delete cascade, and files in Storage cannot be deleted from SQL - remove the user's objects through the Storage API first.

9. Verify users on your server

Never trust a user id sent by the client. Send the access token as a Bearer header and resolve it on the server with supabase.auth.getUser(jwt), which checks the signature, expiry and that the user still exists.

Read next: RevenueCat subscriptions in an Expo Router app, which ties purchases to the signed-in user, or the Supabase auth module docs.

More guides