Build a Social Auth App with Expo React Native
This tutorial demonstrates how to build a React Native app with Expo that implements social authentication. The app showcases a complete authentication flow with protected navigation using:
- Supabase Database - a Postgres database for storing your user data with Row Level Security to ensure data is protected and users can only access their own information.
- Supabase Auth - enables users to log in through social authentication providers (Apple and Google).
If you get stuck while working through this guide, refer to the full example on GitHub.
Project setup
Before you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a "schema" inside the database.
Create a project
- Create a new project in the Supabase Dashboard.
- Enter your project details.
- Wait for the new database to launch.
Set up the database schema
Now set up the database schema. You can use the "User Management Starter" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.
- Go to the SQL Editor page in the Dashboard.
- Click User Management Starter under the Community > Quickstarts tab.
- Click Run.
You can pull the database schema down to your local project by running the db pull
command. Read the local development docs for detailed instructions.
123supabase link --project-ref <project-id># You can get <project-id> from your project's dashboard URL: https://supabase.com/dashboard/project/<project-id>supabase db pull
Get API details
Now that you've created some database tables, you are ready to insert data using the auto-generated API.
To do this, you need to get the Project URL and key. Get the URL from the API settings section of a project and the key from the the API Keys section of a project's Settings page.
Changes to API keys
Supabase is changing the way keys work to improve project security and developer experience. You can read the full announcement, but in the transition period, you can use both the current anon
and service_role
keys and the new publishable key with the form sb_publishable_xxx
which will replace the older keys.
To get the key values, open the API Keys section of a project's Settings page and do the following:
- For legacy keys, copy the
anon
key for client-side operations and theservice_role
key for server-side operations from the Legacy API Keys tab. - For new keys, open the API Keys tab, if you don't have a publishable key already, click Create new API Keys, and copy the value from the Publishable key section.
Building the app
Start by building the React Native app from scratch.
Initialize a React Native app
Use Expo to initialize an app called expo-social-auth
with the standard template:
123npx create-expo-app@latestcd expo-social-auth
Install the additional dependencies:
- supabase-js
- @react-native-async-storage/async-storage - A key-value store for React Native.
- expo-secure-store - Provides a way to securely store key-value pairs locally on the device.
- expo-splash-screen - Provides a way to programmatically manage the splash screen.
1npx expo install @supabase/supabase-js @react-native-async-storage/async-storage expo-secure-store expo-splash-screen
Now, create a helper file to initialize the Supabase client for both web and React Native platforms using platform-specific storage adapters: Expo SecureStore for mobile and AsyncStorage for web.
1234567891011121314151617181920212223242526272829import AsyncStorage from '@react-native-async-storage/async-storage';import { createClient } from '@supabase/supabase-js';import 'react-native-url-polyfill/auto';const ExpoWebSecureStoreAdapter = { getItem: (key: string) => { console.debug("getItem", { key }) return AsyncStorage.getItem(key) }, setItem: (key: string, value: string) => { return AsyncStorage.setItem(key, value) }, removeItem: (key: string) => { return AsyncStorage.removeItem(key) },};export const supabase = createClient( process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY ?? '', { auth: { storage: ExpoWebSecureStoreAdapter, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, }, },);
Set up environment variables
You need the API URL and the anon
key copied earlier.
These variables are safe to expose in your Expo app since Supabase has Row Level Security enabled on your database.
Create a .env
file containing these variables:
12EXPO_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URLEXPO_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Set up protected navigation
Next, you need to protect app navigation to prevent unauthenticated users from accessing protected routes. Use the Expo SplashScreen
to display a loading screen while fetching the user profile and verifying authentication status.
Create the AuthContext
Create a React context to manage the authentication session, making it accessible from any component:
123456789101112131415161718import { Session } from '@supabase/supabase-js'import { createContext, useContext } from 'react'export type AuthData = { session?: Session | null profile?: any | null isLoading: boolean isLoggedIn: boolean}export const AuthContext = createContext<AuthData>({ session: undefined, profile: undefined, isLoading: true, isLoggedIn: false,})export const useAuthContext = () => useContext(AuthContext)
Create the AuthProvider
Next, create a provider component to manage the authentication session throughout the app:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879import { AuthContext } from '@/hooks/use-auth-context'import { supabase } from '@/lib/supabase'import type { Session } from '@supabase/supabase-js'import { PropsWithChildren, useEffect, useState } from 'react'export default function AuthProvider({ children }: PropsWithChildren) { const [session, setSession] = useState<Session | undefined | null>() const [profile, setProfile] = useState<any>() const [isLoading, setIsLoading] = useState<boolean>(true) // Fetch the session once, and subscribe to auth state changes useEffect(() => { const fetchSession = async () => { setIsLoading(true) const { data: { session }, error, } = await supabase.auth.getSession() if (error) { console.error('Error fetching session:', error) } setSession(session) setIsLoading(false) } fetchSession() const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { console.log('Auth state changed:', { event: _event, session }) setSession(session) }) // Cleanup subscription on unmount return () => { subscription.unsubscribe() } }, []) // Fetch the profile when the session changes useEffect(() => { const fetchProfile = async () => { setIsLoading(true) if (session) { const { data } = await supabase .from('profiles') .select('*') .eq('id', session.user.id) .single() setProfile(data) } else { setProfile(null) } setIsLoading(false) } fetchProfile() }, [session]) return ( <AuthContext.Provider value={{ session, isLoading, profile, isLoggedIn: session != undefined, }} > {children} </AuthContext.Provider> )}
Create the SplashScreenController
Create a SplashScreenController
component to display the Expo SplashScreen
while the authentication session is loading:
1234567891011121314import { useAuthContext } from '@/hooks/use-auth-context'import { SplashScreen } from 'expo-router'SplashScreen.preventAutoHideAsync()export function SplashScreenController() { const { isLoading } = useAuthContext() if (!isLoading) { SplashScreen.hideAsync() } return null}
Create a logout component
Create a logout button component to handle user sign-out:
And add it to the app/(tabs)/index.tsx
file used to display the user profile data and the logout button:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556import { Image } from 'expo-image'import { StyleSheet } from 'react-native'import { HelloWave } from '@/components/hello-wave'import ParallaxScrollView from '@/components/parallax-scroll-view'import { ThemedText } from '@/components/themed-text'import { ThemedView } from '@/components/themed-view'import SignOutButton from '@/components/social-auth-buttons/sign-out-button'import { useAuthContext } from '@/hooks/use-auth-context'export default function HomeScreen() { const { profile } = useAuthContext() return ( <ParallaxScrollView headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} headerImage={ <Image source={require('@/assets/images/partial-react-logo.png')} style={styles.reactLogo} /> } > <ThemedView style={styles.titleContainer}> <ThemedText type="title">Welcome!</ThemedText> <HelloWave /> </ThemedView> <ThemedView style={styles.stepContainer}> <ThemedText type="subtitle">Username</ThemedText> <ThemedText>{profile?.username}</ThemedText> <ThemedText type="subtitle">Full name</ThemedText> <ThemedText>{profile?.full_name}</ThemedText> </ThemedView> <SignOutButton /> </ParallaxScrollView> )}const styles = StyleSheet.create({ titleContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, }, stepContainer: { gap: 8, marginBottom: 8, }, reactLogo: { height: 178, width: 290, bottom: 0, left: 0, position: 'absolute', },})
Create a login screen
Next, create a basic login screen component:
1234567891011121314151617181920212223242526272829303132import { Link, Stack } from 'expo-router'import { StyleSheet } from 'react-native'import { ThemedText } from '@/components/themed-text'import { ThemedView } from '@/components/themed-view'export default function LoginScreen() { return ( <> <Stack.Screen options={{ title: 'Login' }} /> <ThemedView style={styles.container}> <ThemedText type="title">Login</ThemedText> <Link href="/" style={styles.link}> <ThemedText type="link">Try to navigate to home screen!</ThemedText> </Link> </ThemedView> </> )}const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20, }, link: { marginTop: 15, paddingVertical: 15, },})
Implement protected routes
Wrap the navigation with the AuthProvider
and SplashScreenController
.
Using Expo Router's protected routes, you can secure navigation:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'import { useFonts } from 'expo-font'import { Stack } from 'expo-router'import { StatusBar } from 'expo-status-bar'import 'react-native-reanimated'import { SplashScreenController } from '@/components/splash-screen-controller'import { useAuthContext } from '@/hooks/use-auth-context'import { useColorScheme } from '@/hooks/use-color-scheme'import AuthProvider from '@/providers/auth-provider'// Separate RootNavigator so we can access the AuthContextfunction RootNavigator() { const { isLoggedIn } = useAuthContext() return ( <Stack> <Stack.Protected guard={isLoggedIn}> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> </Stack.Protected> <Stack.Protected guard={!isLoggedIn}> <Stack.Screen name="login" options={{ headerShown: false }} /> </Stack.Protected> <Stack.Screen name="+not-found" /> </Stack> )}export default function RootLayout() { const colorScheme = useColorScheme() const [loaded] = useFonts({ SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), }) if (!loaded) { // Async font loading only occurs in development. return null } return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <AuthProvider> <SplashScreenController /> <RootNavigator /> <StatusBar style="auto" /> </AuthProvider> </ThemeProvider> )}
You can now test the app by running:
12npx expo prebuildnpx expo start --clear
Verify that the app works as expected. The splash screen displays while fetching the user profile, and the login page appears even when attempting to navigate to the home screen using the Link
button.
By default Supabase Auth requires email verification before a session is created for the user. To support email verification you need to implement deep link handling!
While testing, you can disable email confirmation in your project's email auth provider settings.
Integrate social authentication
Now integrate social authentication with Supabase Auth, starting with Apple authentication. If you only need to implement Google authentication, you can skip to the Google authentication section.
Apple authentication
Start by adding the button inside the login screen:
12345678910111213141516...import AppleSignInButton from '@/components/social-auth-buttons/apple/apple-sign-in-button';...export default function LoginScreen() { return ( <> <Stack.Screen options={{ title: 'Login' }} /> <ThemedView style={styles.container}> ... <AppleSignInButton /> ... </ThemedView> </> );}...
For Apple authentication, you can choose between:
- Invertase's React Native Apple Authentication library - that supports iOS, Android
- react-apple-signin-auth - that supports Web, also suggested by Invertase
- Expo's AppleAuthentication library - that supports iOS only
For either option, you need to obtain a Service ID from the Apple Developer Console.
To enable Apple sign-up on Android and Web, you also need to register the tunnelled URL (e.g., https://arnrer1-anonymous-8081.exp.direct
) obtained by running:
1npx expo start --tunnel
And add it to the Redirect URLs field in your Supabase dashboard Authentication configuration.
For more information, follow the Supabase Login with Apple guide.
Prerequisites
Before proceeding, ensure you have followed the Invertase prerequisites documented in the Invertase Initial Setup Guide and the Invertase Android Setup Guide.
You need to add two new environment variables to the .env
file:
12EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID="YOUR_APPLE_AUTH_SERVICE_ID"EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI="YOUR_APPLE_AUTH_REDIRECT_URI"
iOS
Install the @invertase/react-native-apple-authentication
library:
1npx expo install @invertase/react-native-apple-authentication
Then create the iOS specific button component AppleSignInButton
:
To test functionality on the simulator, remove the getCredentialStateForUser
check:
Enable the Apple authentication capability in iOS:
1234567891011{ "expo": { ... "ios": { ... "usesAppleSignIn": true ... }, ... }}
Add the capabilities to the Info.plist
file by following the Expo documentation.
Before testing the app, if you've already built the iOS app, clean the project artifacts:
1npx react-native-clean-project clean-project-auto
If issues persist, try completely cleaning the cache, as reported by many users in this closed issue.
Finally, update the iOS project by installing the Pod library and running the Expo prebuild command:
1234cd iospod installcd ..npx expo prebuild
Now test the application on a physical device:
1npx expo run:ios --no-build-cache --device
You should see the login screen with the Apple authentication button.
If you get stuck while working through this guide, refer to the full Invertase example on GitHub.
Android
Install the required libraries:
1npx expo install @invertase/react-native-apple-authentication react-native-get-random-values uuid
Next, create the Android-specific AppleSignInButton
component:
You should now be able to test the authentication by running it on a physical device or simulator:
1npx expo run:android --no-build-cache
Google authentication
Start by adding the button to the login screen:
12345678910111213141516...import GoogleSignInButton from '@/components/social-auth-buttons/google/google-sign-in-button';...export default function LoginScreen() { return ( <> <Stack.Screen options={{ title: 'Login' }} /> <ThemedView style={styles.container}> ... <GoogleSignInButton /> ... </ThemedView> </> );}...
For Google authentication, you can choose between the following options:
- GN Google Sign In Premium - that supports iOS, Android, and Web by using the latest Google's One Tap sign-in (but it requires a subscription)
- @react-oauth/google - that supports Web (so it's not a good option for mobile, but it works)
- Relying on the ``signInWithOAuth function of the Supabase Auth - that also supports iOS, Android and Web (useful also to manage any other OAuth provider)
The GN Google Sign In Free doesn't support iOS or Android, as it doesn't allow to pass a custom nonce to the sign-in request.
For either option, you need to obtain a Web Client ID from the Google Cloud Engine, as explained in the Google Sign In guide.
This guide only uses the @react-oauth/google@latest option for the Web, and the signInWithOAuth
for the mobile platforms.
Before proceeding, add a new environment variable to the .env
file:
1EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID="YOUR_GOOGLE_AUTH_WEB_CLIENT_ID"
Install the @react-oauth/google
library:
1npx expo install @react-oauth/google
Enable the expo-web-browser
plugin in app.json
:
12345678910111213141516{ "expo": { ... "plugins": { ... [ "expo-web-browser", { "experimentalLauncherActivity": false } ] ... }, ... }}
Then create the iOS specific button component GoogleSignInButton
:
Test the authentication in your browser using the tunnelled HTTPS URL:
1npx expo start --tunnel
To allow the Google Sign In to work, as you did before for Apple, you need to register the tunnelled URL (e.g., https://arnrer1-anonymous-8081.exp.direct
) obtained to the Authorized JavaScript origins list of your Google Cloud Console's OAuth 2.0 Client IDs configuration.