Expo Quickstart
Before you start
Example repositories
There are three approaches for adding authentication to your Expo app.
Use the following tabs to choose your preferred approach:
This approach uses built with React Native components and works in Expo Go — no dev build required.
Enable Native API
In the Clerk Dashboard, navigate to the Native applications page and ensure that the Native API is enabled. This is required to integrate Clerk in your native application.
Create a new Expo app
If you don't already have an Expo app, run the following commands to create a new one.
npx create-expo-app@latest clerk-expo
cd clerk-expopnpm dlx create-expo-app@latest clerk-expo
cd clerk-expoyarn dlx create-expo-app@latest clerk-expo
cd clerk-expobun x create-expo-app@latest clerk-expo
cd clerk-expoRemove default template files
The default Expo template includes files that will conflict with the routes you'll create in this guide. Remove the conflicting files and unused components/ directory:
rm -rf "app/(tabs)" app/modal.tsx app/+not-found.tsx components/The default template also includes react-native-reanimated, which can cause known Android build issues. Since it's not needed for this guide, remove it to avoid build errors:
npm uninstall react-native-reanimated react-native-worklets --legacy-peer-depsThen, remove the reanimated import from app/_layout.tsx:
import 'react-native-reanimated';Install dependencies
Install the required packages. Use npx expo install to ensure SDK-compatible versions.
- The Clerk Expo SDK
gives you access to prebuilt components, hooks, and helpers to make user authentication easier.Expo Icon - Clerk stores the active user's session token in memory by default. In Expo apps, the recommended way to store sensitive data, such as tokens, is by using
expo-secure-storewhich encrypts the data before storing it.
npx expo install @clerk/expo expo-secure-storeAdd your Clerk to your .env file.
- In the Clerk Dashboard, navigate to the API keys page.
- In the Quick Copy section, copy your Clerk .
- Paste your key into your
.envfile.
The final result should resemble the following:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEYAdd <ClerkProvider> to your root layout
The <ClerkProvider> component provides session and user context to Clerk's hooks and components. It's recommended to wrap your entire app at the entry point with <ClerkProvider> to make authentication globally accessible. See the reference docs for other configuration options.
Add the component to your root layout and pass your and tokenCache from @clerk/expo/token-cache as props, as shown in the following example:
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
throw new Error('Add your Clerk Publishable Key to the .env file')
}
export default function RootLayout() {
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}Add sign-up and sign-in pages
Clerk currently only supports control components for Expo native. UI components are only available for Expo web. Instead, you must build using Clerk's API. The following sections demonstrate how to build custom email/password sign-up and sign-in flows. If you want to use different authentication methods, such as passwordless or OAuth, see the dedicated custom flow guides.
Layout page
First, protect your sign-up and sign-in pages.
- Create an
(auth)route group. This will group your sign-up and sign-in pages. - In the
(auth)group, create a_layout.tsxfile with the following code. The useAuth() hook is used to access the user's authentication state. If the user is already signed in, they will be redirected to the home page.
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'
export default function AuthRoutesLayout() {
const { isSignedIn, isLoaded } = useAuth()
if (!isLoaded) {
return null
}
if (isSignedIn) {
return <Redirect href={'/'} />
}
return <Stack />
}Sign-up page
In the (auth) group, create a sign-up.tsx file with the following code. The useSignUp() hook is used to create a sign-up flow. The user can sign up using their email and password and will receive an email verification code to confirm their email.
import { useAuth, useSignUp } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
export default function Page() {
const { signUp, errors, fetchStatus } = useSignUp()
const { isSignedIn } = useAuth()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [password, setPassword] = React.useState('')
const [code, setCode] = React.useState('')
const handleSubmit = async () => {
const { error } = await signUp.password({
emailAddress,
password,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (!error) await signUp.verifications.sendEmailCode()
}
const handleVerify = async () => {
await signUp.verifications.verifyEmailCode({
code,
})
if (signUp.status === 'complete') {
await signUp.finalize({
// Redirect the user to the home page after signing up
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else {
// Check why the sign-up is not complete
console.error('Sign-up attempt not complete:', signUp)
}
}
if (signUp.status === 'complete' || isSignedIn) {
return null
}
if (
signUp.status === 'missing_requirements' &&
signUp.unverifiedFields.includes('email_address') &&
signUp.missingFields.length === 0
) {
return (
<View style={styles.container}>
<Text style={[styles.title, { fontSize: 24, fontWeight: 'bold' }]}>
Verify your account
</Text>
<TextInput
style={styles.input}
value={code}
placeholder="Enter your verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors.fields.code && <Text style={styles.error}>{errors.fields.code.message}</Text>}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleVerify}
disabled={fetchStatus === 'fetching'}
>
<Text style={styles.buttonText}>Verify</Text>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signUp.verifications.sendEmailCode()}
>
<Text style={styles.secondaryButtonText}>I need a new code</Text>
</Pressable>
</View>
)
}
return (
<View style={styles.container}>
<Text style={[styles.title, { fontSize: 24, fontWeight: 'bold' }]}>Sign up</Text>
<Text style={styles.label}>Email address</Text>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
keyboardType="email-address"
/>
{errors.fields.emailAddress && (
<Text style={styles.error}>{errors.fields.emailAddress.message}</Text>
)}
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && <Text style={styles.error}>{errors.fields.password.message}</Text>}
<Pressable
style={({ pressed }) => [
styles.button,
(!emailAddress || !password || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleSubmit}
disabled={!emailAddress || !password || fetchStatus === 'fetching'}
>
<Text style={styles.buttonText}>Sign up</Text>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <Text style={styles.debug}>{JSON.stringify(errors, null, 2)}</Text>}
<View style={styles.linkContainer}>
<Text>Already have an account? </Text>
<Link href="/sign-in">
<Text style={{ color: '#0a7ea4' }}>Sign in</Text>
</Link>
</View>
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
<View nativeID="clerk-captcha" />
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
secondaryButton: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
secondaryButtonText: {
color: '#0a7ea4',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
})Sign-in page
In the (auth) group, create a sign-in.tsx file with the following code. The useSignIn() hook is used to create a sign-in flow. The user can sign in using email address and password, or navigate to the sign-up page.
import { useSignIn } from '@clerk/expo'
import { type Href, Link, useRouter } from 'expo-router'
import React from 'react'
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
export default function Page() {
const { signIn, errors, fetchStatus } = useSignIn()
const router = useRouter()
const [emailAddress, setEmailAddress] = React.useState('')
const [password, setPassword] = React.useState('')
const [code, setCode] = React.useState('')
const handleSubmit = async () => {
const { error } = await signIn.password({
emailAddress,
password,
})
if (error) {
console.error(JSON.stringify(error, null, 2))
return
}
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else if (signIn.status === 'needs_second_factor' || signIn.status === 'needs_client_trust') {
// Handle second factor or client trust verification
// For other second factor strategies,
// see https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
// see https://clerk.com/docs/guides/development/custom-flows/authentication/client-trust
const emailCodeFactor = signIn.supportedSecondFactors.find(
(factor) => factor.strategy === 'email_code',
)
if (emailCodeFactor) {
await signIn.mfa.sendEmailCode()
}
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
const handleVerify = async () => {
await signIn.mfa.verifyEmailCode({ code })
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Handle pending session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
console.log(session?.currentTask)
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url as Href)
}
},
})
} else {
// Check why the sign-in is not complete
console.error('Sign-in attempt not complete:', signIn)
}
}
if (signIn.status === 'needs_second_factor' || signIn.status === 'needs_client_trust') {
return (
<View style={styles.container}>
<Text style={[styles.title, { fontSize: 24, fontWeight: 'bold' }]}>
Verify your account
</Text>
<TextInput
style={styles.input}
value={code}
placeholder="Enter your verification code"
placeholderTextColor="#666666"
onChangeText={(code) => setCode(code)}
keyboardType="numeric"
/>
{errors.fields.code && <Text style={styles.error}>{errors.fields.code.message}</Text>}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleVerify}
disabled={fetchStatus === 'fetching'}
>
<Text style={styles.buttonText}>Verify</Text>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.mfa.sendEmailCode()}
>
<Text style={styles.secondaryButtonText}>I need a new code</Text>
</Pressable>
</View>
)
}
return (
<View style={styles.container}>
<Text style={[styles.title, { fontSize: 24, fontWeight: 'bold' }]}>Sign in</Text>
<Text style={styles.label}>Email address</Text>
<TextInput
style={styles.input}
autoCapitalize="none"
value={emailAddress}
placeholder="Enter email"
placeholderTextColor="#666666"
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
keyboardType="email-address"
/>
{errors.fields.identifier && (
<Text style={styles.error}>{errors.fields.identifier.message}</Text>
)}
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && <Text style={styles.error}>{errors.fields.password.message}</Text>}
<Pressable
style={({ pressed }) => [
styles.button,
(!emailAddress || !password || fetchStatus === 'fetching') && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleSubmit}
disabled={!emailAddress || !password || fetchStatus === 'fetching'}
>
<Text style={styles.buttonText}>Continue</Text>
</Pressable>
{/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */}
{errors && <Text style={styles.debug}>{JSON.stringify(errors, null, 2)}</Text>}
<View style={styles.linkContainer}>
<Text>Don't have an account? </Text>
<Link href="/sign-up">
<Text style={{ color: '#0a7ea4' }}>Sign up</Text>
</Link>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 12,
},
title: {
marginBottom: 8,
},
label: {
fontWeight: '600',
fontSize: 14,
},
input: {
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonDisabled: {
opacity: 0.5,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
secondaryButton: {
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
secondaryButtonText: {
color: '#0a7ea4',
fontWeight: '600',
},
linkContainer: {
flexDirection: 'row',
gap: 4,
marginTop: 12,
alignItems: 'center',
},
error: {
color: '#d32f2f',
fontSize: 12,
marginTop: -8,
},
debug: {
fontSize: 10,
opacity: 0.5,
marginTop: 8,
},
})For more information about building these , including guided comments in the code examples, see the Build a custom email/password authentication flow guide.
Add a home screen
You can control which content signed-in and signed-out users can see with Clerk's prebuilt control components. For this guide, you'll use:
- <Show when="signed-in">: Children of this component can only be seen while signed in.
- <Show when="signed-out">: Children of this component can only be seen while signed out.
- Create a
(home)route group. - In the
(home)group, create a_layout.tsxfile with the following code.
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'
export default function Layout() {
const { isSignedIn, isLoaded } = useAuth()
if (!isLoaded) {
return null
}
if (!isSignedIn) {
return <Redirect href="/(auth)/sign-in" />
}
return <Stack />
}Then, in the same folder, create an index.tsx file. If the user is signed in, it displays their email and a sign-out button. If they're not signed in, it displays sign-in and sign-up links.
import { Show, useUser } from '@clerk/expo'
import { useClerk } from '@clerk/expo'
import { Link } from 'expo-router'
import { Text, View, Pressable, StyleSheet } from 'react-native'
export default function Page() {
const { user } = useUser()
const { signOut } = useClerk()
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome!</Text>
<Show when="signed-out">
<Link href="/(auth)/sign-in">
<Text>Sign in</Text>
</Link>
<Link href="/(auth)/sign-up">
<Text>Sign up</Text>
</Link>
</Show>
<Show when="signed-in">
<Text>Hello {user?.emailAddresses[0].emailAddress}</Text>
<Pressable style={styles.button} onPress={() => signOut()}>
<Text style={styles.buttonText}>Sign out</Text>
</Pressable>
</Show>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
paddingTop: 60,
gap: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
})Run your project
Run your project with the following command:
npx expo startThen use the terminal shortcuts to run the app on your preferred platform:
- Press
ito open the iOS simulator. - Press
ato open the Android emulator. - Scan the QR code with Expo Go to run the app on a physical device.
Create your first user
Once the app opens on your device or simulator:
- Navigate to the Sign up screen.
- Enter your details and complete the authentication flow.
- After signing up, your first user will be created and you'll be signed in.
Native sign-in with Google and Apple (optional)
If you want to add native Sign in with Google and Sign in with Apple buttons that authenticate without opening a browser, you'll need to install expo-crypto:
npx expo install expo-cryptoThen, refer to the Sign in with Google and Sign in with Apple guides for full setup instructions, including any additional dependencies specific to each provider. This approach requires a development build because it uses native modules. It cannot run in Expo Go.
This approach uses Clerk's pre-built native components
Enable Native API
In the Clerk Dashboard, navigate to the Native applications page and ensure that the Native API is enabled. This is required to integrate Clerk in your native application.
Create a new Expo app
If you don't already have an Expo app, run the following commands to create a new one.
npx create-expo-app@latest clerk-expo
cd clerk-expopnpm dlx create-expo-app@latest clerk-expo
cd clerk-expoyarn dlx create-expo-app@latest clerk-expo
cd clerk-expobun x create-expo-app@latest clerk-expo
cd clerk-expoRemove default template files
The default Expo template includes files that will conflict with the routes you'll create in this guide. Remove the conflicting files and unused components/ directory:
rm -rf "app/(tabs)" app/modal.tsx app/+not-found.tsx components/The default template also includes react-native-reanimated, which can cause known Android build issues. Since it's not needed for this guide, remove it to avoid build errors:
npm uninstall react-native-reanimated react-native-worklets --legacy-peer-depsThen, remove the reanimated import from app/_layout.tsx:
import 'react-native-reanimated';Install dependencies
Install the required packages. Use npx expo install to ensure SDK-compatible versions.
- The Clerk Expo SDK
gives you access to prebuilt components, hooks, and helpers to make user authentication easier.Expo Icon - Clerk stores the active user's session token in memory by default. In Expo apps, the recommended way to store sensitive data, such as tokens, is by using
expo-secure-storewhich encrypts the data before storing it. expo-auth-sessionhandles authentication redirects and OAuth flows in Expo apps.expo-web-browseropens the system browser during authentication and returns the user to the app once the flow is complete.expo-dev-clientallows you to build and run your app in development mode.
npx expo install @clerk/expo expo-secure-store expo-auth-session expo-web-browser expo-dev-clientAdd your Clerk to your .env file.
- In the Clerk Dashboard, navigate to the API keys page.
- In the Quick Copy section, copy your Clerk .
- Paste your key into your
.envfile.
The final result should resemble the following:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEYVerify app.json plugins
Run npx expo install to automatically add the required config plugins to your app.json file. Then verify that @clerk/expo and expo-secure-store appear in the plugins array:
{
"expo": {
"plugins": ["expo-secure-store", "@clerk/expo"]
}
}Add <ClerkProvider> to your root layout
The <ClerkProvider> component provides session and user context to Clerk's hooks and components. It's recommended to wrap your entire app at the entry point with <ClerkProvider> to make authentication globally accessible. See the reference docs for other configuration options.
Add the component to your root layout and pass your and tokenCache from @clerk/expo/token-cache as props, as shown in the following example:
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
throw new Error('Add your Clerk Publishable Key to the .env file')
}
export default function RootLayout() {
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}Add authentication and home screen
With native components
Create an index.tsx file in your app folder with the following code. If the user is signed in, it displays their email, a profile button, and a sign-out button. If they're not signed in, it displays the <AuthView /> component which handles both sign-in and sign-up.
import { useAuth, useUser, useClerk, useUserProfileModal } from '@clerk/expo'
import { AuthView, UserButton } from '@clerk/expo/native'
import { Text, View, StyleSheet, Image, TouchableOpacity, ActivityIndicator } from 'react-native'
export default function MainScreen() {
const { isSignedIn, isLoaded } = useAuth()
const { user } = useUser()
const { signOut } = useClerk()
const { presentUserProfile } = useUserProfileModal()
if (!isLoaded) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" />
</View>
)
}
if (!isSignedIn) {
return <AuthView mode="signInOrUp" />
}
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Welcome</Text>
<View style={{ width: 44, height: 44, borderRadius: 22, overflow: 'hidden' }}>
<UserButton />
</View>
</View>
<View style={styles.profileCard}>
{user?.imageUrl && <Image source={{ uri: user.imageUrl }} style={styles.avatar} />}
<View>
<Text style={styles.name}>
{user?.firstName || 'User'} {user?.lastName || ''}
</Text>
<Text style={styles.email}>{user?.emailAddresses[0]?.emailAddress}</Text>
</View>
</View>
<TouchableOpacity style={styles.linkButton} onPress={presentUserProfile}>
<Text style={styles.linkButtonText}>Manage Profile</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.linkButton, { backgroundColor: '#666' }]}
onPress={() => signOut()}
>
<Text style={styles.linkButtonText}>Sign Out</Text>
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
padding: 40,
},
container: {
flex: 1,
backgroundColor: '#fff',
padding: 20,
paddingTop: 60,
gap: 16,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
fontSize: 28,
fontWeight: 'bold',
},
profileCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: '#f5f5f5',
borderRadius: 12,
gap: 12,
},
avatar: {
width: 48,
height: 48,
borderRadius: 24,
},
name: {
fontSize: 18,
fontWeight: '600',
},
email: {
fontSize: 14,
color: '#666',
},
linkButton: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 12,
alignItems: 'center',
},
linkButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
})Build and run
This approach requires a development build because it uses native modules. It cannot run in Expo Go.
# Using Expo CLI
npx expo run:ios
npx expo run:android
# Using EAS Build
eas build --platform ios
eas build --platform android
# Or using local prebuild
npx expo prebuild && npx expo run:ios --device
npx expo prebuild && npx expo run:android --deviceThen use the terminal shortcuts to run the app on your preferred platform:
- Press
ito open the iOS simulator. - Press
ato open the Android emulator. - Scan the QR code with Expo Go to run the app on a physical device.
Create your first user
Once the app opens on your device or simulator:
- Navigate to the Sign up screen.
- Enter your details and complete the authentication flow.
- After signing up, your first user will be created and you'll be signed in.
Configure social connections (optional)
<AuthView /> automatically shows sign-in buttons for any social connections enabled in your Clerk Dashboard. However, native OAuth requires additional credential setup — without it, the buttons will appear but fail with an error when tapped.
Sign in with Google
Follow the steps in the Sign in with Google guide to complete the following:
- Enable Google as a social connection with Use custom credentials toggled on.
- Create OAuth 2.0 credentials in the Google Cloud Console — you'll need an iOS Client ID, Android Client ID, and Web Client ID.
- Set the Web Client ID and Client Secret in the Clerk Dashboard.
- Add your iOS application to the Native Applications page in the Clerk Dashboard (Team ID + Bundle ID).
- Add your Android application to the Native Applications page in the Clerk Dashboard (package name).
- Add the Google Client IDs as environment variables in your
.envfile. Follow the.env.examplein the Sign in with Google guide. - Configure the
@clerk/expoplugin with the iOS URL scheme in yourapp.json.
Sign in with Apple
Follow the steps in the Sign in with Apple guide to complete the following:
- Add your iOS application to the Native Applications page in the Clerk Dashboard (Team ID + Bundle ID).
- Enable Apple as a social connection in the Clerk Dashboard.
Enable OTA updates
Though not required, it is recommended to implement over-the-air (OTA) updates in your Expo app. This enables you to easily roll out Clerk's feature updates and security patches as they're released without having to resubmit your app to mobile marketplaces.
See the expo-updates library to learn how to get started.
Next steps
Learn more about Clerk prebuilt components, custom flows for your native apps, and how to deploy an Expo app to production using the following guides.
Create a custom sign-up and sign-in flow
Learn how to build a custom sign-up and sign-in authentication flow.
Prebuilt native componentsBeta
Learn how to quickly add authentication to your app using Clerk's pre-built native UI for iOS and Android.
Protect content and read user data
Learn how to use Clerk's hooks and helpers to protect content and read user data in your Expo app.
Custom flows
Expo native apps require custom flows in place of prebuilt components.
Deploy an Expo app to production
Learn how to deploy your Expo app to production.
Clerk Expo SDK Reference
Learn about the Clerk Expo SDK and how to integrate it into your app.
Feedback
Last updated on