Supabase Auth: The Ultimate Authentication Solution for Cross-Platform Apps using React Native
Auth solution for React native apps

After facing the limitation for an auth package web support, I was looking for auth solutions to have a platform based smooth authentication experience. So the major authentication methods I wanted in the app I was building was,
email+ password
apple sign in (iOS native)
google signin (android native)
I wanted to have all these methods available to me on all the targeted platforms (android, iOS and web) for our react-native app. I knew a lot of auth libraries for web, and a lot of libraries for mobile only, and I've been following Supabase's launch week, for quite a while.
when the decision to move on with web build support for the app came in, I dived right in supabase-auth and ended up with a working product that supported native social logins on respective native platforms and browser based logins on web, for our react native application.
Supabase auth is more than an auth service, it provides you dashboard to manage users and implement Row Level Security for it's users. Auth hooks are an added feature to implement custom behavior on user creation. Check more here:
https://supabase.com/docs/guides/auth/auth-hooks
How to setup auth in an Expo app
We are using Tamagui(ui lib) in the app, so make sure to install it.yarn add @tamagui
Let me just share the code snippets and a walkthrough tutorial of how you can achieve the same set of authentication, which to me sounds ideal.
Install the following packages to setup supabase client, storage and secure store for cross-platform expo app.
yarn add @supabase/supabase-js expo-crypto expo-secure-store react-native-mmkv
Create a storage.ts and supabase.ts under /utils folder
import { createClient } from '@supabase/supabase-js';
import { AppState } from 'react-native';
import 'react-native-url-polyfill/auto';
import * as SessionStorage from './storage';
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: SessionStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});
export { type AuthError, type Session } from '@supabase/supabase-js';
/**
* Tells Supabase to autorefresh the session while the application
* is in the foreground. (Docs: https://supabase.com/docs/reference/javascript/auth-startautorefresh)
*/
AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'active') {
supabase.auth.startAutoRefresh();
} else {
supabase.auth.stopAutoRefresh();
}
});
import * as Crypto from 'expo-crypto';
import * as SecureStore from 'expo-secure-store';
import { MMKV } from 'react-native-mmkv';
import { isWeb } from 'tamagui';
const fetchOrGenerateEncryptionKey = (): string => {
const encryptionKey = SecureStore.getItem('session-encryption-key');
if (encryptionKey) {
return encryptionKey;
} else {
const uuid = Crypto.randomUUID();
SecureStore.setItem('session-encryption-key', uuid);
return uuid;
}
};
export const storage = new MMKV({
id: 'session',
encryptionKey: !isWeb ? fetchOrGenerateEncryptionKey() : undefined,
});
// TODO: Remove this workaround for encryption: https://github.com/mrousavy/react-native-mmkv/issues/665
storage.set('workaround', true);
/**
* A simple wrapper around MMKV that provides a base API
* that matches AsyncStorage for use with Supabase.
*/
/**
* Get an item from storage by key
*
* @param {string} key of the item to fetch
* @returns {Promise<string | null>} value for the key as a string or null if not found
*/
export async function getItem(key: string): Promise<string | null> {
try {
return storage.getString(key) ?? null;
} catch {
console.warn(`Failed to get key "${key}" from secure storage`);
return null;
}
}
/**
* Sets an item in storage by key
*
* @param {string} key of the item to store
* @param {string} value of the item to store
*/
export async function setItem(key: string, value: string): Promise<void> {
try {
storage.set(key, value);
} catch {
console.warn(`Failed to set key "${key}" in secure storage`);
}
}
/**
* Removes a single item from storage by key
*
* @param {string} key of the item to remove
*/
export async function removeItem(key: string): Promise<void> {
try {
storage.delete(key);
} catch {
console.warn(`Failed to remove key "${key}" from secure storage`);
}
}
Then create an Auth Component that has conditional, native first segregation of auth providers, via supabase.
Install the following packages yarn add expo-auth-session expo-web-browser
//Auth.tsx
import { makeRedirectUri } from 'expo-auth-session';
import * as QueryParams from 'expo-auth-session/build/QueryParams';
import * as WebBrowser from 'expo-web-browser';
import { useState } from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';
import { useMMKVObject, useMMKVString } from 'react-native-mmkv';
import { captureSentryException } from 'sentry_telemetry/sentryLogger';
import { Stack, XStack, YStack } from 'tamagui';
import MyInput from '~/components/basic/TextInput';
import { colors } from '~/constants/color';
import { fontSize } from '~/constants/fontSize';
import { supabase } from '~/utils/supabase';
import DottedLine from '../DottedLine';
import Text from '../basic/Text';
import { Button } from '../button';
import Icon from '../icon/Icon';
import WebBasedGoogleLogin from './WebBasedGoogleLogin';
WebBrowser.maybeCompleteAuthSession(); // required for web only
const redirectTo = makeRedirectUri({});
const createSessionFromUrl = async (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 { data, error } = await supabase.auth.setSession({
access_token,
refresh_token,
});
if (error) throw error;
return data.session;
};
type AuthProps = {
isAndroid: boolean;
isWeb: boolean;
isIos: boolean;
};
export default function Auth({ isAndroid, isIos, isWeb }: AuthProps) {
const toast = useToastController();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [loginOrSignup, setLoginOrSignup] = useState<'Login' | 'SignUp'>('Login');
const [accessToken, setAccessToken] = useMMKVString('access_token');
const [refreshToken, setRefreshToken] = useMMKVString('refresh_token');
console.log('accessToken value:', accessToken);
const [mmkvUser, setMMKVUser] = useMMKVObject<Employee>('mmkv_user');
const [upsertUser] = useUpsertUserMutation();
async function signInWithEmail() {
setLoading(true);
try {
const {
error,
data: { user, session },
} = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) {
throw new Error(error.message);
}
if (session?.access_token) {
upsertUser({
fetchPolicy: 'network-only',
refetchQueries: [{ query: GetLoggedInUserDocument }],
onCompleted(data) {
setMMKVUser(data?.register);
setAccessToken(session.access_token);
// TODO: Handle successful user upsert
},
onError(error) {
console.log('error', error);
captureSentryException('Error in creating new user', error as any);
},
});
}
} catch (error) {
// TODO: Handle error in sign-in with email
} finally {
setLoading(false);
}
}
const performOAuthGoogle = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: redirectTo,
skipBrowserRedirect: true,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
scopes: '',
},
});
console.log('OauthGoolge', data);
if (error) throw error;
console.log('OauthGoolg', data?.url, redirectTo);
const res = await WebBrowser.openAuthSessionAsync(data?.url ?? '', redirectTo);
if (res.type === 'success') {
const { url } = res;
console.log(url);
const credential = await createSessionFromUrl(url);
console.log('after successful token generation', credential);
setAccessToken(credential?.access_token);
setRefreshToken(credential?.refresh_token);
// handleSignInWithGoogle(credential);
}
};
const performOAuthApple = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'apple',
options: {
redirectTo,
skipBrowserRedirect: true,
},
});
console.log(data?.url);
if (error) throw error;
console.log(error);
const res = await WebBrowser.openAuthSessionAsync(data?.url ?? '', redirectTo);
if (res.type === 'success') {
const { url } = res;
console.log(url);
const credential = await createSessionFromUrl(url);
console.log(credential?.access_token);
setAccessToken(credential?.access_token);
setRefreshToken(credential?.refresh_token);
// handleSignInWithGoogle(credential);
}
};
// async function handleSignInWithGoogle(session: Session) {
// const { data, error } = await supabase.auth.signInWithIdToken({
// provider: 'google',
// token: response.credential,
// nonce: 'NONCE', // must be the same one as provided in data-nonce (if any)
// });
// }
async function signUpWithEmail() {
setLoading(true);
try {
const {
data: { session },
error,
} = await supabase.auth.signUp({
email: email,
password: password,
});
if (error) {
throw new Error(error.message);
}
console.log(session?.user);
if (!session) {
// TODO: Show email verification prompt
}
} catch (error) {
// TODO: Handle error in sign-up with email
} finally {
setLoading(false);
}
}
return (
<Stack w={'80%'} alignSelf="center">
<YStack w={'90%'} gap={10} alignSelf="center">
<MyInput
label="Email"
// leftIcon={{ type: 'font-awesome', name: 'envelope' }}
onChangeText={(text) => setEmail(text)}
value={email}
placeholder="Email"
autoCapitalize={'none'}
/>
<MyInput
label="Password"
// leftIcon={{ type: 'font-awesome', name: 'lock' }}
onChangeText={(text) => setPassword(text)}
value={password}
secureTextEntry={true}
placeholder="Password"
autoCapitalize={'none'}
/>
{loginOrSignup === 'Login' ? (
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button
backgroundColor={!email || !password ? '$background' : '$primary'}
disabled={!email || !password}
onPress={() => signInWithEmail()}>
{loading ? (
<ActivityIndicator color={colors.primary} />
) : (
<Text variant="semi-bold" color={!email || !password ? 'black' : 'white'}>
Log in
</Text>
)}
</Button>
</View>
) : (
<View style={styles.verticallySpaced}>
<Button
backgroundColor={!email || !password ? '$background' : '$primary'}
disabled={!email || !password}
onPress={() => signUpWithEmail()}>
{loading ? (
<ActivityIndicator color={colors.primary} />
) : (
<Text variant="semi-bold" color={!email || !password ? 'black' : 'white'}>
Register
</Text>
)}
</Button>
</View>
)}
</YStack>
{isIos && !isAndroid ? (
<WebBasedGoogleLogin />
) : (
<View style={styles.verticallySpaced}>
<Button
w={'70%'}
onPress={performOAuthGoogle}
alignSelf="center"
style={styles.googleBtn}>
<Text color={'white'} variant="bold">
Sign in With Google
</Text>
</Button>
</View>
)}
{isWeb && (
<View style={styles.verticallySpaced}>
<Button w={'70%'} alignSelf="center" onPress={performOAuthApple} style={styles.appleBtn}>
<Text color={'white'} variant="semi-bold">
Sign in With Apple
</Text>
</Button>
</View>
)}
<DottedLine />
<XStack gap={4} alignSelf="center">
<Text
onPress={() => setLoginOrSignup(loginOrSignup === 'Login' ? 'SignUp' : 'Login')}
style={{
textAlign: 'center',
color: colors.primary,
fontSize: fontSize.lg,
fontWeight: '500',
}}>
{loginOrSignup === 'Login' ? 'Sign Up' : 'Log in'}
</Text>
</XStack>
</Stack>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
},
emailInput: {
marginBottom: 10,
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: 'stretch',
},
mt20: {
marginTop: 20,
},
appleBtn: {
backgroundColor: colors.black,
borderRadius: 100,
height: 50,
},
googleBtn: {
backgroundColor: colors.orange,
borderRadius: 100,
height: 50,
},
});
Now create respective, NativeAppleSignIn.tsx and NativeGoogleSignIn.tsx. Install the required packages by running yarn add expo-apple-authentication @react-native-google-signin/google-signin
//NativeGoogleSignIn.tsx
import {
GoogleSignin,
GoogleSigninButton,
statusCodes,
} from '@react-native-google-signin/google-signin';
import 'core-js/stable/atob';
import {
Employee,
GetLoggedInUserDocument,
useRegisterUserMutation,
} from 'generated/hooks_and_more';
import jwtDecode from 'jwt-decode';
import { Platform } from 'react-native';
import { useMMKVObject, useMMKVString } from 'react-native-mmkv';
import { captureSentryException } from 'sentry_telemetry/sentryLogger';
import { supabase } from '~/utils/supabase';
export default function GoogleSignIn() {
const [accessToken, setAccessToken] = useMMKVString('access_token');
const [refreshToken, setRefreshToken] = useMMKVString('refresh_token');
const [mmkvUser, setMMKVUser] = useMMKVObject<Employee>('mmkv_user');
const [registerUser, { data: userResponse, loading: registerLoading }] =
useRegisterUserMutation();
GoogleSignin.configure({
iosClientId: process.env.EXPO_PUBLIC_IOS_GOOGLE_CLIENT_ID,
webClientId: process.env.EXPO_PUBLIC_WEB_GOOGLE_CLIENT_ID,
offlineAccess: true,
scopes: ['profile', 'email'],
});
async function onPress(){
try {
await GoogleSignin.hasPlayServices();
const userInfo = await GoogleSignin.signIn();
const decodedToken = jwtDecode(userInfo.idToken);
const nonceFromToken = decodedToken.nonce;
console.log(nonceFromToken);
if (userInfo.idToken) {
const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'google',
token: userInfo.idToken,
});
console.log('google native', error, data);
setAccessToken(data?.session?.access_token || '');
setRefreshToken(data?.session?.refresh_token || '');
registerUser({
refetchQueries: [{ query: GetLoggedInUserDocument }],
onCompleted(data, clientOptions) {
setMMKVUser(data?.register);
// TODO: Handle successful user registration
},
onError(error, clientOptions) {
console.log('error', error);
captureSentryException(
'Error in creating new user[Native Google SignIn]',
error as any
);
},
});
} else {
throw new Error('no ID token present!');
}
} catch (error: any) {
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
console.log('user cancelled the sign in', error);
} else if (error.code === statusCodes.IN_PROGRESS) {
console.log('user cancelled the login flow', error);
} else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
console.log('play services not available or outdated', error);
} else {
console.log('some other error happened', error);
}
}
}}
if (Platform.OS === 'android') {
return (
<GoogleSigninButton
size={GoogleSigninButton.Size.Standard}
color={GoogleSigninButton.Color.Dark}
onPress={onPress}
/>
);
}
return null;
}
//NativeAppleSignIn.tsx
import * as AppleAuthentication from 'expo-apple-authentication';
import {
Employee,
GetLoggedInUserDocument,
useRegisterUserMutation,
} from 'generated/hooks_and_more';
import { Platform } from 'react-native';
import { useMMKVObject, useMMKVString } from 'react-native-mmkv';
import { captureSentryException } from 'sentry_telemetry/sentryLogger';
import { supabase } from '~/utils/supabase';
export default function AppleSignIn() {
const [accessToken, setAccessToken] = useMMKVString('access_token');
const [refreshToken, setRefreshToken] = useMMKVString('refresh_token');
const [mmkvUser, setMMKVUser] = useMMKVObject<Employee>('mmkv_user');
const [registerUser, { data: userResponse, loading: registerLoading }] =
useRegisterUserMutation();
const handleAppleSignIn = async () => {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
console.log(credential);
if (credential.identityToken) {
const {
error,
data: { user, session },
} = await supabase.auth.signInWithIdToken({
provider: 'apple',
token: credential.identityToken,
});
console.log(JSON.stringify({ error, user }, null, 2));
if (!error) {
setAccessToken(session?.access_token || '');
setRefreshToken(session?.refresh_token || '');
registerUser({
refetchQueries: [{ query: GetLoggedInUserDocument }],
onCompleted(data, clientOptions) {
setMMKVUser(data?.register);
console.log('on registered in Native Apple Login', accessToken);
},
onError(error, clientOptions) {
console.log('error', error);
captureSentryException('Error in creating new user', error as any);
},
});
}
} else {
throw new Error('No identityToken.');
}
} catch (e) {
if (e.code === 'ERR_REQUEST_CANCELED') {
// handle that the user canceled the sign-in flow
} else {
// handle other errors
}
}
};
if (Platform.OS === 'ios') {
return (
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={5}
style={{ width: 200, height: 64 }}
onPress={handleAppleSignIn}
/>
);
}
return <>{/* Implement Android Auth options. */}</>;
}
There was an issue that I face with native Google sign in on iOS for quite long and chose to drop it for a while and use an alternative that supports cross-platform auth.
For that, create aWebBasedGoogleLogin.tsxfile and use it in theAuthcomponent universally. Add the packages by running
//WebBasedGoogleLogin.tsx
import * as Google from 'expo-auth-session/providers/google';
import * as WebBrowser from 'expo-web-browser';
import {
Employee,
GetLoggedInUserDocument,
useRegisterUserMutation,
} from 'generated/hooks_and_more';
import { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import { useMMKVObject, useMMKVString } from 'react-native-mmkv';
import { captureSentryException } from 'sentry_telemetry/sentryLogger';
import { setRefreshToken } from '~/hooks/storage/token';
import { supabase } from '~/utils/supabase';
import Text from '../basic/Text';
import { Button } from '../button';
WebBrowser.maybeCompleteAuthSession();
export default function WebBasedGoogleLogin() {
const [userInfo, setUserInfo] = useState();
const [mmkvUser, setMMKVUser] = useMMKVObject('mmkv_user');
const [accessToken, setAccessToken] = useMMKVString('access_token');
const [registerUser, { data: userResponse, loading: registerLoading }] =
useRegisterUserMutation();
const [request, response, promptAsync] = Google.useAuthRequest({
androidClientId: process.env.EXPO_PUBLIC_ANDROID_GOOGLE_CLIENT_ID,
webClientId: process.env.EXPO_PUBLIC_WEB_GOOGLE_CLIENT_ID,
iosClientId: process.env.EXPO_PUBLIC_IOS_GOOGLE_CLIENT_ID,
});
useEffect(() => {
// setAccessToken('');
handleSignInWithGoogle();
}, [response]);
async function handleSignInWithGoogle() {
if (!mmkvUser) {
if (response?.type === 'success') {
const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'google',
token: response?.authentication?.idToken,
});
setAccessToken(data?.session?.access_token);
setRefreshToken(data?.session?.refresh_token);
setMMKVUser(data?.session?.user);
}
} else {
setMMKVUser('');
//error message
}
}
const getUserInfo = async (token) => {
if (!token) return;
try {
const res = await fetch('https://www.googleapis.com/userinfo/v2/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const user = await res.json();
console.log(user);
//db upsert user
registerUser({
refetchQueries: [{ query: GetLoggedInUserDocument }],
onCompleted(data, clientOptions) {
setMMKVUser(data?.register);
setUserInfo(data?.register);
// user && getAccessToken() && navigation.replace('SetupYourOrganization');
},
onError(error, clientOptions) {
console.log('error', error);
captureSentryException('Error in creating new user', error as any);
},
});
} catch (error) {}
};
return (
<>
<Text>{JSON.stringify(userInfo)}</Text>
<Button w={'70%'} alignSelf="center" onPress={() => promptAsync()} style={styles.googleBtn}>
<Text variant="semi-bold" color={'white'}>
Sign in with Google
</Text>
</Button>
</>
);
}
const styles = StyleSheet.create({
googleBtn: {
backgroundColor: '#4285F4', // Google's brand color
borderRadius: 4,
paddingVertical: 12,
paddingHorizontal: 16,
elevation: 3,
},
});
All of these components only work, if I pass the accessToken in the headers of my fetcher function, and to do secure API calls, add this to your fetcher headers,
const JWT_TOKEN = (await supabase.auth.getSession()).data.session?.access_token;
Make sure to add the following environment variables
//.env
EXPO_PUBLIC_SUPABASE_URL=
EXPO_PUBLIC_SUPABASE_ANON_KEY=
EXPO_PUBLIC_IOS_GOOGLE_CLIENT_ID=
EXPO_PUBLIC_WEB_GOOGLE_CLIENT_ID=
EXPO_PUBLIC_ANDROID_GOOGLE_CLIENT_ID=
EXPO_PUBLIC_SUPABASE_REDIRECT_URI=
I've followed the guides available on various platforms and github issues, to be able to work on this. Supabase-Auth is a marvelous library, it has every major auth provider of the internet, Supabase auth client is written in go, please do give it a ⭐ on github.
Thanks for reading this tutorial, If you found it useful. Let me know. If you have any doubts, do reach out to me on twitter @Agrit Tiwari
Cheers!





