Expo Quickstart
Before you start
Example repositories
There are three approaches for adding authentication to your Expo app.
| Approach | Auth UI | OAuth | Requires dev build | Best for |
|---|---|---|---|---|
| Native components | Pre-built native components | Native (no browser) | Yes | Fastest integration |
| JS + Native sign-in | + native OAuth buttons | Native (no browser) | Yes | Custom UI with native Sign in with Google/Apple |
| JavaScript | Browser-based | No (works in Expo Go) | Full control over UI |
Use the following tabs to choose your preferred approach:
Warning
Expo native components are currently in beta. If you run into any issues, please reach out to the support team.
This approach uses Clerk's pre-built native components that render using SwiftUI on iOS and Jetpack Compose on Android. This requires the least code and a development build.
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.
npxcreate-expo-app@latestclerk-expo
cdclerk-expopnpmdlxcreate-expo-app@latestclerk-expo
cdclerk-expoyarndlxcreate-expo-app@latestclerk-expo
cdclerk-expobunxcreate-expo-app@latestclerk-expo
cdclerk-expoRemove the starter routes
The default Expo template includes starter routes that aren't used in this guide. Remove them:
rm-fsrc/app/index.tsxsrc/app/explore.tsxThis guide replaces the starter src/app/_layout.tsx file and creates the app routes in later steps.
Note
If your Expo app uses a root app folder instead of src/app, use the same file paths without src/.
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.
- 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-dev-clientallows you to build and run your app in development mode.
npxexpoinstall@clerk/expoexpo-secure-storeexpo-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'
constpublishableKey=process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
thrownewError('Add your Clerk Publishable Key to the .env file')
}
exportdefaultfunctionRootLayout() {
return (
<ClerkProviderpublishableKey={publishableKey} tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}Add authentication and home screen
With native components, you can build a complete app in a single file. The <AuthView /> component handles all sign-in and sign-up flows, and <UserButton /> provides a profile avatar that opens the native user profile.
Create a src/app/index.tsx file with the following code. If the user is signed in, it displays the <UserButton />. If they're not signed in, it displays a Sign in button that opens the <AuthView />.
Important
When using native components, pass { treatPendingAsSignedOut: false } to useAuth() so pending are not treated as signed out.
Important
Keep the React Native <Modal> that contains <AuthView /> mounted at the same level as your signed-in and signed-out content. Don't render the modal only inside signed-out content, because auth state can change before required are finished and unmount the modal too early.
import { useAuth } from'@clerk/expo'
import { AuthView, UserButton } from'@clerk/expo/native'
import { useState } from'react'
import { View, StyleSheet, ActivityIndicator, Button, Modal } from'react-native'
exportdefaultfunctionMainScreen() {
const { isSignedIn,isLoaded } =useAuth({ treatPendingAsSignedOut:false })
const [isAuthOpen,setIsAuthOpen] =useState(false)
if (!isLoaded) {
return (
<Viewstyle={styles.centered}>
<ActivityIndicatorsize="large" />
</View>
)
}
return (
<Viewstyle={styles.container}>
{isSignedIn ? <UserButton /> : <Buttontitle="Sign in"onPress={() =>setIsAuthOpen(true)} />}
<Modal
animationType="slide"
visible={isAuthOpen}
presentationStyle="pageSheet"
onRequestClose={() =>setIsAuthOpen(false)}
>
<AuthViewonDismiss={() =>setIsAuthOpen(false)} />
</Modal>
</View>
)
}
conststyles=StyleSheet.create({
centered: {
flex:1,
justifyContent:'center',
alignItems:'center',
},
container: {
flex:1,
justifyContent:'center',
alignItems:'center',
},
})Build and run
This approach requires a development build because it uses native modules. It cannot run in Expo Go.
# Using Expo CLI
npxexporun:ios
npxexporun:android
# Using EAS Build
easbuild--platformios
easbuild--platformandroid
# Or using local prebuild
npxexpoprebuild&&npxexporun:ios--device
npxexpoprebuild&&npxexporun: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:
- Open the sign-up flow.
- 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.
Important
You do not need to install expo-crypto or use the useSignInWithGoogle() hook — <AuthView /> handles the sign-in flow automatically.
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.
Important
You do not need to install expo-apple-authentication, expo-crypto, or use the useSignInWithApple() hook — <AuthView /> handles the sign-in flow automatically.
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.
npxcreate-expo-app@latestclerk-expo
cdclerk-expopnpmdlxcreate-expo-app@latestclerk-expo
cdclerk-expoyarndlxcreate-expo-app@latestclerk-expo
cdclerk-expobunxcreate-expo-app@latestclerk-expo
cdclerk-expoRemove the starter routes
The default Expo template includes starter routes that aren't used in this guide. Remove them:
rm-fsrc/app/index.tsxsrc/app/explore.tsxThis guide replaces the starter src/app/_layout.tsx file and creates the app routes in later steps.
Note
If your Expo app uses a root app folder instead of src/app, use the same file paths without src/.
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.
- 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.
npxexpoinstall@clerk/expoexpo-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'
constpublishableKey=process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
thrownewError('Add your Clerk Publishable Key to the .env file')
}
exportdefaultfunctionRootLayout() {
return (
<ClerkProviderpublishableKey={publishableKey} tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}Add sign-up and sign-in pages
For JavaScript-only Expo native apps that run in Expo Go, use Clerk's control components and build with Clerk's API. UI components are available for Expo web, and native components are available for iOS and Android apps that use a development build. 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'
exportdefaultfunctionAuthRoutesLayout() {
const { isSignedIn,isLoaded } =useAuth()
if (!isLoaded) {
returnnull
}
if (isSignedIn) {
return <Redirecthref={'/'} />
}
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 { 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 up
navigate: ({ session, decorateUrl }) => {
// Handle session tasks
// See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
if (session?.currentTask) {
console.log(session?.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
consturl=decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url asHref)
}
},
})
} else {
// Check why the sign-up is not complete
console.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>
<TextInput
style={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>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus ==='fetching'&&styles.buttonDisabled,
pressed &&styles.buttonPressed,
]}
onPress={handleVerify}
disabled={fetchStatus ==='fetching'}
>
<ThemedTextstyle={styles.buttonText}>Verify</ThemedText>
</Pressable>
<Pressable
style={({ 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>
<TextInput
style={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>
<TextInput
style={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>
)}
<Pressable
style={({ 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,
},
})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 { 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-tasks
if (session?.currentTask) {
console.log(session?.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
consturl=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-trust
constemailCodeFactor=signIn.supportedSecondFactors.find(
(factor) =>factor.strategy ==='email_code',
)
if (emailCodeFactor) {
awaitsignIn.mfa.sendEmailCode()
}
} else {
// Check why the sign-in is not complete
console.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-tasks
if (session?.currentTask) {
console.log(session?.currentTask)
return
}
// If no session tasks, navigate the signed-in user to the home page
consturl=decorateUrl('/')
if (url.startsWith('http')) {
window.location.href = url
} else {
router.push(url asHref)
}
},
})
} else {
// Check why the sign-in is not complete
console.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>
<TextInput
style={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>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus ==='fetching'&&styles.buttonDisabled,
pressed &&styles.buttonPressed,
]}
onPress={handleVerify}
disabled={fetchStatus ==='fetching'}
>
<ThemedTextstyle={styles.buttonText}>Verify</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed &&styles.buttonPressed]}
onPress={() =>signIn.mfa.sendEmailCode()}
>
<ThemedTextstyle={styles.secondaryButtonText}>I need a new code</ThemedText>
</Pressable>
<Pressable
style={({ 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>
<TextInput
style={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>
<TextInput
style={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>
)}
<Pressable
style={({ 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,
},
})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'
exportdefaultfunctionLayout() {
const { isSignedIn,isLoaded } =useAuth()
if (!isLoaded) {
returnnull
}
if (!isSignedIn) {
return <Redirecthref="/(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'
exportdefaultfunctionPage() {
const { user } =useUser()
const { signOut } =useClerk()
return (
<Viewstyle={styles.container}>
<Textstyle={styles.title}>Welcome!</Text>
<Showwhen="signed-out">
<Linkhref="/(auth)/sign-in">
<Text>Sign in</Text>
</Link>
<Linkhref="/(auth)/sign-up">
<Text>Sign up</Text>
</Link>
</Show>
<Showwhen="signed-in">
<Text>Hello {user?.id}</Text>
<Pressablestyle={styles.button} onPress={() =>signOut()}>
<Textstyle={styles.buttonText}>Sign out</Text>
</Pressable>
</Show>
</View>
)
}
conststyles=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:
npxexpostartThen 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:
- Open the sign-up flow.
- 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, 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.
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, and how to deploy an Expo app to production using the following guides.
Prebuilt native componentsBeta
Learn how to quickly add authentication to your app using Clerk's pre-built native UI for iOS and Android.
Custom flows
Build custom authentication flows with Clerk's API when prebuilt components don't fit your product.
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.
Deploy an Expo app to production
Learn how to deploy your Expo app to production.
Feedback
Last updated on
