Skip to content
All articles

Complete Guide: Sign in with Apple, Supabase & Expo (2026)

Nov 29, 2025· Updated Nov 3, 2026 9 min read
  • Auth
  • Apple
  • Supabase
  • Expo
  • Security

To add Sign in with Apple to an Expo app that uses Supabase, turn on the Sign in with Apple capability for your App ID, install expo-apple-authentication, and pass the identityToken it returns to supabase.auth.signInWithIdToken({ provider: 'apple', token }). For native iOS sign-in, Supabase only needs your bundle ID in the Apple provider's Client IDs list. The Services ID and the .p8 secret key are only needed for web or Android sign-in.

This guide covers the native iOS flow, the extra setup for web and Android, the first-sign-in name rule, and what App Review expects when a user deletes their account. Code samples match the Expo SDK 58 and Supabase docs as of November 2026.

Does Apple require Sign in with Apple?

Not always. App Review Guideline 4.8 says that if your app uses a third-party or social login (Google, Facebook, X and so on) to set up the user's primary account, you must also offer an equivalent login service that:

  • limits data collection to the user's name and email address,
  • lets users keep their email address private, and
  • doesn't collect app interactions for advertising without consent.

Sign in with Apple meets all of those, which is why most apps with Google Sign-In add it. You don't need another login service if your app only uses your own account system, is an education or enterprise app that requires an existing org account, uses a government ID system, or is a client for a specific third-party service. Read the current wording in Apple's App Review Guidelines before you decide.

What you need first

  • Apple Developer Program membership ($99 a year) to enable the capability on a real bundle ID.
  • An Expo project. Expo SDK 58 is current (released October 15, 2026). expo-apple-authentication works the same way on recent SDKs.
  • A Supabase project. The free plan is fine for development.
  • A way to build for iOS. EAS Build works without a Mac. If you build locally, note that App Store Connect only accepts builds made with Xcode 26 or later (Apple's rule since April 28, 2026).
  • A real iPhone for final testing. Expo's docs say you can do limited testing on the iOS Simulator, but some methods behave differently there. getCredentialStateAsync always throws on the simulator.

You can try the flow in Expo Go on iOS without any Apple setup. The tokens will name Expo Go's App ID (host.exp.Exponent) as their audience, so add that to Supabase while you test (more on this below).

Step 1: Turn on the capability for your App ID

  1. Open Certificates, Identifiers & Profiles → Identifiers.
  2. Select your App ID, or click + to register one with your bundle ID (for example com.yourcompany.yourapp).
  3. Check Sign In with Apple and save. Leave the server-to-server notification endpoint blank: Supabase Auth doesn't support it yet.

If you build with EAS, you can skip the manual step. EAS Build syncs capabilities on the Apple Developer Console with the entitlements in your app config every time it builds. It also works the other way: a capability that is on in the portal but missing from your entitlements gets switched off. So set usesAppleSignIn in app.json (Step 3) and let the build keep the two in step.

Step 2: Configure the Apple provider in Supabase

  1. In the Supabase dashboard, open Authentication → Providers and enable Apple.
  2. In Client IDs, list every bundle ID that will send tokens: your production bundle ID, any variants your EAS profiles use (such as com.yourcompany.yourapp.dev), and host.exp.Exponent if you test in Expo Go.
  3. Save. For a native-only app, that's the whole Supabase side. No secret key, no rotation.

Only for web or Android sign-in: Services ID and secret key

Apple has no native SDK on Android or the web, so those platforms use Supabase's OAuth flow, which needs more:

  1. Create a Services ID under Identifiers (for example com.yourcompany.yourapp.web), enable Sign in with Apple on it, and set the domain to <project-ref>.supabase.co with the return URL https://<project-ref>.supabase.co/auth/v1/callback.
  2. Create a key under Keys with Sign in with Apple checked. Download the .p8 file. Apple lets you download it once.
  3. Generate the client secret (a signed JWT) from that key, your Team ID and the Key ID. Supabase's Apple guide has a generator; any JWT library works too.
  4. In Supabase, put the Services ID first in Client IDs, then your bundle IDs. Supabase uses the first entry for the web flow. Paste the generated secret into the secret field.

Calendar reminder: Apple caps the client secret's lifetime at six months. When it expires, web and Android Apple sign-in stops working until you generate a new one. Native iOS sign-in is not affected.

Your Team ID is the 10-character string shown in the Membership details of your developer account. The Key ID is shown next to the key on the Keys page.

Step 3: Install and configure expo-apple-authentication

npx expo install expo-apple-authentication expo-crypto

Then set the capability flag and add the config plugin in app.json:

{
  "expo": {
    "scheme": "yourapp",
    "ios": {
      "bundleIdentifier": "com.yourcompany.yourapp",
      "usesAppleSignIn": true
    },
    "plugins": ["expo-apple-authentication"]
  }
}

Older tutorials add an Info.plist usage string here. Sign in with Apple doesn't need one. After changing app.json, make a new development build: the capability lives in the native binary, so a JavaScript reload won't pick it up.

Step 4: Create the Supabase client

Install the client and the storage it uses on React Native:

npx expo install @supabase/supabase-js react-native-url-polyfill expo-sqlite

This is the setup from Supabase's Expo quickstart. supabase-js saves and refreshes the session itself, so you don't need to write the session to AsyncStorage by hand (many older guides do, and end up with two copies that drift apart).

// lib/supabase.ts
import 'react-native-url-polyfill/auto';
import 'expo-sqlite/localStorage/install';
import { AppState } from 'react-native';
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
  {
    auth: {
      storage: localStorage,
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false,
    },
  }
);

// Refresh tokens only while the app is in the foreground.
AppState.addEventListener('change', (state) => {
  if (state === 'active') supabase.auth.startAutoRefresh();
  else supabase.auth.stopAutoRefresh();
});

The publishable key is meant to ship in the app. Never put a secret or service role key in client code.

Step 5: Add the button and sign in

The component below shows Apple's native button, sends a hashed nonce to Apple and the raw nonce to Supabase (so a captured token can't be replayed), and saves the user's name on the first sign-in.

// components/AppleSignInButton.tsx
import { Platform } from 'react-native';
import * as AppleAuthentication from 'expo-apple-authentication';
import * as Crypto from 'expo-crypto';
import { supabase } from '../lib/supabase';

export function AppleSignInButton() {
  if (Platform.OS !== 'ios') return null;

  const signIn = async () => {
    try {
      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,
      });

      if (!credential.identityToken) {
        throw new Error('Apple did not return an identity token');
      }

      const { error } = await supabase.auth.signInWithIdToken({
        provider: 'apple',
        token: credential.identityToken,
        nonce: rawNonce,
      });
      if (error) throw error;

      // Apple sends the name only on the first sign-in. Save it now.
      const given = credential.fullName?.givenName;
      const family = credential.fullName?.familyName;
      if (given || family) {
        await supabase.auth.updateUser({
          data: {
            full_name: [given, family].filter(Boolean).join(' '),
            given_name: given,
            family_name: family,
          },
        });
      }
    } catch (e: any) {
      if (e.code === 'ERR_REQUEST_CANCELED') return; // user closed the sheet
      console.error('Apple sign-in failed', e);
    }
  };

  return (
    <AppleAuthentication.AppleAuthenticationButton
      buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
      buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
      cornerRadius={8}
      style={{ width: 260, height: 44 }}
      onPress={signIn}
    />
  );
}

Notes on the button: give it an explicit width and height or it won't render, and don't restyle it with backgroundColor or borderRadius. Use buttonStyle and cornerRadius instead, which keeps it within Apple's branding rules. You can check AppleAuthentication.isAvailableAsync() before rendering it.

Step 6: Read the session anywhere

// hooks/useSession.ts
import { useEffect, useState } from 'react';
import type { Session } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';

export function useSession() {
  const [session, setSession] = useState<Session | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => {
      setSession(data.session);
      setLoading(false);
    });
    const { data } = supabase.auth.onAuthStateChange((_event, next) => {
      setSession(next);
    });
    return () => data.subscription.unsubscribe();
  }, []);

  return { session, loading, signOut: () => supabase.auth.signOut() };
}

The name and email rules that surprise people

Apple returns the user's full name and email only the first time they authorize your app. On every later sign-in, fullName comes back null, and Apple's identity token never contains the name at all. That's why Step 5 saves it right away with updateUser. If you miss it, the only ways to get it again are to ask the user, or have them remove your app from the Sign in with Apple list in their Apple Account settings and sign in again.

Users can also hide their email. You'll then get an address ending in @privaterelay.appleid.com that forwards to them. To send mail to those addresses, register your sending domains and addresses under Sign in with Apple for Email Communication in your developer account; otherwise Apple's relay drops the messages.

export function isPrivateRelay(email: string | null | undefined) {
  return !!email && email.endsWith('@privaterelay.appleid.com');
}

Account deletion and token revocation

Guideline 5.1.1(v) says any app that lets people create an account must let them start deleting it inside the app. Deactivating isn't enough. For Sign in with Apple users there's one more step: Apple's account deletion FAQ says apps should call the Sign in with Apple REST API to revoke the user's tokens when the account is deleted. That call needs your client secret, so make it from a server or a Supabase Edge Function, not the app. Apple's page Offering account deletion in your app covers the rest, including how to treat users with active subscriptions.

Troubleshooting

"Unacceptable audience in id_token"

The token's audience (the bundle ID that requested it) isn't in Supabase's Client IDs list. Add the exact bundle ID, including host.exp.Exponent for Expo Go and any .dev or .preview variants.

Nonce errors from Supabase

Send the SHA-256 hash to Apple and the raw value to Supabase. Swapping them, or sending a nonce to only one side, fails the check.

The Apple sheet never appears, or errors with code 1000

Usually the build doesn't have the capability. Confirm usesAppleSignIn: true, rebuild, and check that the device or simulator is signed in to an Apple Account.

Web or Android Apple sign-in broke after six months

The client secret expired. Generate a new one from the same .p8 key and paste it into Supabase.

Useful commands

# Print the resolved app config, including entitlements
npx expo config --type introspect

# Build and run on a connected iPhone
npx expo run:ios --device

Before you submit

  • Use Apple's button component, or a custom button that follows Apple's sign-in button guidelines.
  • Make sure the account deletion flow works for Apple users, including token revocation.
  • Mention Apple sign-in data in your privacy policy and your App Privacy answers in App Store Connect.
  • If your Supabase project also signs in web users with Apple, set a reminder for the secret rotation.

Where AppSetUpKit fits

AppSetUpKit doesn't include a sign-in screen or auth hooks; you write those from the steps above. Its setup wizard can create your App Store Connect app record and turn on the Sign in with Apple capability through Fastlane with your own App Store Connect API key, and it installs expo-apple-authentication. The kit's supabase-auth-setup.sh turns on the Apple provider in your Supabase project through the Management API, and generate-apple-jwt.js creates the client secret that web flows need. See what's in the kit.

More guides