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.
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:
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:
Then, remove the reanimated import from app/_layout.tsx:
- import'react-native-reanimated';
Important
You can skip this step if you used npx create-expo-app@latest --template blank to create your app. However, the blank template doesn't include Expo Router or pre-styled UI components. You'll need to install expo-router and its dependencies to follow along with this guide.
Install the required packages. Use npx expo install to ensure SDK-compatible versions.
The Clerk Expo SDKExpo Icon gives you access to prebuilt components, hooks, and helpers to make user authentication easier.
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-store which encrypts the data before storing it.
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 Publishable Key and tokenCache from @clerk/expo/token-cache as props, as shown in the following example:
Clerk currently only supports control components for Expo native. UI components are only available for Expo web. Instead, you must build custom flows 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.
Create an (auth)route group. This will group your sign-up and sign-in pages.
In the (auth) group, create a _layout.tsx file 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.
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.
app/(auth)/sign-up.tsx
import { ThemedText } from'@/components/themed-text'import { ThemedView } from'@/components/themed-view'import { useAuth, useSignUp } from'@clerk/expo'import { type Href, Link, useRouter } from'expo-router'import React from'react'import { Pressable, StyleSheet, TextInput, View } from'react-native'exportdefaultfunctionPage() {const { signUp,errors,fetchStatus } =useSignUp()const { isSignedIn } =useAuth()constrouter=useRouter()const [emailAddress,setEmailAddress] =React.useState('')const [password,setPassword] =React.useState('')const [code,setCode] =React.useState('')consthandleSubmit=async () => {const { error } =awaitsignUp.password({ emailAddress, password, })if (error) {console.error(JSON.stringify(error,null,2))return }if (!error) awaitsignUp.verifications.sendEmailCode() }consthandleVerify=async () => {awaitsignUp.verifications.verifyEmailCode({ code, })if (signUp.status ==='complete') {awaitsignUp.finalize({// Redirect the user to the home page after signing upnavigate: ({ session, decorateUrl }) => {// Handle session tasks// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasksif (session?.currentTask) {console.log(session?.currentTask)return }// If no session tasks, navigate the signed-in user to the home pageconsturl=decorateUrl('/')if (url.startsWith('http')) {window.location.href = url } else {router.push(url asHref) } }, }) } else {// Check why the sign-up is not completeconsole.error('Sign-up attempt not complete:', signUp) } }if (signUp.status ==='complete'|| isSignedIn) {returnnull }if (signUp.status ==='missing_requirements'&&signUp.unverifiedFields.includes('email_address') &&signUp.missingFields.length===0 ) {return ( <ThemedViewstyle={styles.container}> <ThemedTexttype="title"style={styles.title}> Verify your account </ThemedText> <TextInputstyle={styles.input}value={code}placeholder="Enter your verification code"placeholderTextColor="#666666"onChangeText={(code) =>setCode(code)}keyboardType="numeric" /> {errors.fields.code && ( <ThemedTextstyle={styles.error}>{errors.fields.code.message}</ThemedText> )} <Pressablestyle={({ pressed }) => [styles.button, fetchStatus ==='fetching'&&styles.buttonDisabled, pressed &&styles.buttonPressed, ]}onPress={handleVerify}disabled={fetchStatus ==='fetching'} > <ThemedTextstyle={styles.buttonText}>Verify</ThemedText> </Pressable> <Pressablestyle={({ pressed }) => [styles.secondaryButton, pressed &&styles.buttonPressed]}onPress={() =>signUp.verifications.sendEmailCode()} > <ThemedTextstyle={styles.secondaryButtonText}>I need a new code</ThemedText> </Pressable> </ThemedView> ) }return ( <ThemedViewstyle={styles.container}> <ThemedTexttype="title"style={styles.title}> Sign up </ThemedText> <ThemedTextstyle={styles.label}>Email address</ThemedText> <TextInputstyle={styles.input}autoCapitalize="none"value={emailAddress}placeholder="Enter email"placeholderTextColor="#666666"onChangeText={(emailAddress) =>setEmailAddress(emailAddress)}keyboardType="email-address" /> {errors.fields.emailAddress && ( <ThemedTextstyle={styles.error}>{errors.fields.emailAddress.message}</ThemedText> )} <ThemedTextstyle={styles.label}>Password</ThemedText> <TextInputstyle={styles.input}value={password}placeholder="Enter password"placeholderTextColor="#666666"secureTextEntry={true}onChangeText={(password) =>setPassword(password)} /> {errors.fields.password && ( <ThemedTextstyle={styles.error}>{errors.fields.password.message}</ThemedText> )} <Pressablestyle={({ pressed }) => [styles.button, (!emailAddress ||!password || fetchStatus ==='fetching') &&styles.buttonDisabled, pressed &&styles.buttonPressed, ]}onPress={handleSubmit}disabled={!emailAddress ||!password || fetchStatus ==='fetching'} > <ThemedTextstyle={styles.buttonText}>Sign up</ThemedText> </Pressable> {/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */} {errors && <ThemedTextstyle={styles.debug}>{JSON.stringify(errors,null,2)}</ThemedText>} <Viewstyle={styles.linkContainer}> <ThemedText>Already have an account? </ThemedText> <Linkhref="/sign-in"> <ThemedTexttype="link">Sign in</ThemedText> </Link> </View> {/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */} <ViewnativeID="clerk-captcha" /> </ThemedView> )}conststyles=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, },})
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.
app/(auth)/sign-in.tsx
import { ThemedText } from'@/components/themed-text'import { ThemedView } from'@/components/themed-view'import { useSignIn } from'@clerk/expo'import { type Href, Link, useRouter } from'expo-router'import React from'react'import { Pressable, StyleSheet, TextInput, View } from'react-native'exportdefaultfunctionPage() {const { signIn,errors,fetchStatus } =useSignIn()constrouter=useRouter()const [emailAddress,setEmailAddress] =React.useState('')const [password,setPassword] =React.useState('')const [code,setCode] =React.useState('')consthandleSubmit=async () => {const { error } =awaitsignIn.password({ emailAddress, password, })if (error) {console.error(JSON.stringify(error,null,2))return }if (signIn.status ==='complete') {awaitsignIn.finalize({navigate: ({ session, decorateUrl }) => {// Handle session tasks// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasksif (session?.currentTask) {console.log(session?.currentTask)return }// If no session tasks, navigate the signed-in user to the home pageconsturl=decorateUrl('/')if (url.startsWith('http')) {window.location.href = url } else {router.push(url asHref) } }, }) } elseif (signIn.status ==='needs_second_factor') {// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication } elseif (signIn.status ==='needs_client_trust') {// For other second factor strategies,// see https://clerk.com/docs/guides/development/custom-flows/authentication/client-trustconstemailCodeFactor=signIn.supportedSecondFactors.find( (factor) =>factor.strategy ==='email_code', )if (emailCodeFactor) {awaitsignIn.mfa.sendEmailCode() } } else {// Check why the sign-in is not completeconsole.error('Sign-in attempt not complete:', signIn) } }consthandleVerify=async () => {awaitsignIn.mfa.verifyEmailCode({ code })if (signIn.status ==='complete') {awaitsignIn.finalize({navigate: ({ session, decorateUrl }) => {// Handle session tasks// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasksif (session?.currentTask) {console.log(session?.currentTask)return }// If no session tasks, navigate the signed-in user to the home pageconsturl=decorateUrl('/')if (url.startsWith('http')) {window.location.href = url } else {router.push(url asHref) } }, }) } else {// Check why the sign-in is not completeconsole.error('Sign-in attempt not complete:', signIn) } }if (signIn.status ==='needs_client_trust') {return ( <ThemedViewstyle={styles.container}> <ThemedTexttype="title"style={[styles.title, { fontSize:24, fontWeight:'bold' }]}> Verify your account </ThemedText> <TextInputstyle={styles.input}value={code}placeholder="Enter your verification code"placeholderTextColor="#666666"onChangeText={(code) =>setCode(code)}keyboardType="numeric" /> {errors.fields.code && ( <ThemedTextstyle={styles.error}>{errors.fields.code.message}</ThemedText> )} <Pressablestyle={({ pressed }) => [styles.button, fetchStatus ==='fetching'&&styles.buttonDisabled, pressed &&styles.buttonPressed, ]}onPress={handleVerify}disabled={fetchStatus ==='fetching'} > <ThemedTextstyle={styles.buttonText}>Verify</ThemedText> </Pressable> <Pressablestyle={({ pressed }) => [styles.secondaryButton, pressed &&styles.buttonPressed]}onPress={() =>signIn.mfa.sendEmailCode()} > <ThemedTextstyle={styles.secondaryButtonText}>I need a new code</ThemedText> </Pressable> <Pressablestyle={({ pressed }) => [styles.secondaryButton, pressed &&styles.buttonPressed]}onPress={() =>signIn.reset()} > <ThemedTextstyle={styles.secondaryButtonText}>Start over</ThemedText> </Pressable> </ThemedView> ) }return ( <ThemedViewstyle={styles.container}> <ThemedTexttype="title"style={styles.title}> Sign in </ThemedText> <ThemedTextstyle={styles.label}>Email address</ThemedText> <TextInputstyle={styles.input}autoCapitalize="none"value={emailAddress}placeholder="Enter email"placeholderTextColor="#666666"onChangeText={(emailAddress) =>setEmailAddress(emailAddress)}keyboardType="email-address" /> {errors.fields.identifier && ( <ThemedTextstyle={styles.error}>{errors.fields.identifier.message}</ThemedText> )} <ThemedTextstyle={styles.label}>Password</ThemedText> <TextInputstyle={styles.input}value={password}placeholder="Enter password"placeholderTextColor="#666666"secureTextEntry={true}onChangeText={(password) =>setPassword(password)} /> {errors.fields.password && ( <ThemedTextstyle={styles.error}>{errors.fields.password.message}</ThemedText> )} <Pressablestyle={({ pressed }) => [styles.button, (!emailAddress ||!password || fetchStatus ==='fetching') &&styles.buttonDisabled, pressed &&styles.buttonPressed, ]}onPress={handleSubmit}disabled={!emailAddress ||!password || fetchStatus ==='fetching'} > <ThemedTextstyle={styles.buttonText}>Continue</ThemedText> </Pressable> {/* For your debugging purposes. You can just console.log errors, but we put them in the UI for convenience */} {errors && <ThemedTextstyle={styles.debug}>{JSON.stringify(errors,null,2)}</ThemedText>} <Viewstyle={styles.linkContainer}> <ThemedText>Don't have an account? </ThemedText> <Linkhref="/sign-up"> <ThemedTexttype="link">Sign up</ThemedText> </Link> </View> </ThemedView> )}conststyles=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, },})
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.
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:
npxexpoinstallexpo-crypto
Then, 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.
Warning
Expo native components are currently in beta. If you run into any issues, please reach out to our support team.
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.
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:
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:
Then, remove the reanimated import from app/_layout.tsx:
- import'react-native-reanimated';
Important
You can skip this step if you used npx create-expo-app@latest --template blank to create your app. However, the blank template doesn't include Expo Router or pre-styled UI components. You'll need to install expo-router and its dependencies to follow along with this guide.
Install the required packages. Use npx expo install to ensure SDK-compatible versions.
The Clerk Expo SDKExpo Icon gives you access to prebuilt components, hooks, and helpers to make user authentication easier.
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-store which encrypts the data before storing it.
expo-auth-session handles authentication redirects and OAuth flows in Expo apps.
expo-web-browser opens the system browser during authentication and returns the user to the app once the flow is complete.
expo-dev-client allows you to build and run your app in development mode.
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:
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 Publishable Key and tokenCache from @clerk/expo/token-cache as props, as shown in the following example:
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.
Important
When using native components, pass { treatPendingAsSignedOut: false } to useAuth() to keep auth state in sync with the native SDK and avoid issues with pending session tasks.
This approach requires a development build because it uses native modules. It cannot run in Expo Go.
terminal
# Using Expo CLInpxexporun:iosnpxexporun:android# Using EAS Buildeasbuild--platformioseasbuild--platformandroid# Or using local prebuildnpxexpoprebuild&&npxexporun:ios--devicenpxexpoprebuild&&npxexporun:android--device
Then use the terminal shortcuts to run the app on your preferred platform:
Press i to open the iOS simulator.
Press a to open the Android emulator.
Scan the QR code with Expo Go to run the app on a physical device.
<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.
You do not need to install expo-apple-authentication, expo-crypto, or use the useSignInWithApple() hook — <AuthView /> handles the sign-in flow automatically.
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.