Expo Quickstart
Before you start
Example repository
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-expo
npm installpnpm dlx create-expo-app@latest clerk-expo
cd clerk-expo
pnpm installyarn dlx create-expo-app@latest clerk-expo
cd clerk-expo
yarn installbun x create-expo-app@latest clerk-expo
cd clerk-expo
bun installInstall @clerk/expo and expo-secure-store
- 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.
Run the following command to install the SDK and its required peer dependency for secure token storage:
npm install @clerk/expo expo-secure-storepnpm add @clerk/expo expo-secure-storeyarn add @clerk/expo expo-secure-storebun add @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 { 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'
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 (
<ThemedView style={styles.container}>
<ThemedText type="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 && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleVerify}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signUp.verifications.sendEmailCode()}
>
<ThemedText style={styles.secondaryButtonText}>I need a new code</ThemedText>
</Pressable>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign up
</ThemedText>
<ThemedText style={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 && (
<ThemedText style={styles.error}>{errors.fields.emailAddress.message}</ThemedText>
)}
<ThemedText style={styles.label}>Password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && (
<ThemedText style={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'}
>
<ThemedText style={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 && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
<View style={styles.linkContainer}>
<ThemedText>Already have an account? </ThemedText>
<Link href="/sign-in">
<ThemedText type="link">Sign in</ThemedText>
</Link>
</View>
{/* Required for sign-up flows. Clerk's bot sign-up protection is enabled by default */}
<View nativeID="clerk-captcha" />
</ThemedView>
)
}
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 { 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'
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') {
// See https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
} else if (signIn.status === 'needs_client_trust') {
// For other second factor strategies,
// 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_client_trust') {
return (
<ThemedView style={styles.container}>
<ThemedText type="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 && (
<ThemedText style={styles.error}>{errors.fields.code.message}</ThemedText>
)}
<Pressable
style={({ pressed }) => [
styles.button,
fetchStatus === 'fetching' && styles.buttonDisabled,
pressed && styles.buttonPressed,
]}
onPress={handleVerify}
disabled={fetchStatus === 'fetching'}
>
<ThemedText style={styles.buttonText}>Verify</ThemedText>
</Pressable>
<Pressable
style={({ pressed }) => [styles.secondaryButton, pressed && styles.buttonPressed]}
onPress={() => signIn.mfa.sendEmailCode()}
>
<ThemedText style={styles.secondaryButtonText}>I need a new code</ThemedText>
</Pressable>
</ThemedView>
)
}
return (
<ThemedView style={styles.container}>
<ThemedText type="title" style={styles.title}>
Sign in
</ThemedText>
<ThemedText style={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 && (
<ThemedText style={styles.error}>{errors.fields.identifier.message}</ThemedText>
)}
<ThemedText style={styles.label}>Password</ThemedText>
<TextInput
style={styles.input}
value={password}
placeholder="Enter password"
placeholderTextColor="#666666"
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
/>
{errors.fields.password && (
<ThemedText style={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'}
>
<ThemedText style={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 && <ThemedText style={styles.debug}>{JSON.stringify(errors, null, 2)}</ThemedText>}
<View style={styles.linkContainer}>
<ThemedText>Don't have an account? </ThemedText>
<Link href="/sign-up">
<ThemedText type="link">Sign up</ThemedText>
</Link>
</View>
</ThemedView>
)
}
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 sign-out button
At this point, your users can sign up or in, but they need a way to sign out.
In the app/components/ folder, create a sign-out-button.tsx file with the following code. The useClerk() hook is used to access the signOut() function, which is called when the user clicks the "Sign out" button.
import { ThemedText } from '@/components/themed-text'
import { useClerk } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { Pressable, StyleSheet } from 'react-native'
export const SignOutButton = () => {
// Use `useClerk()` to access the `signOut()` function
const { signOut } = useClerk()
const router = useRouter()
const handleSignOut = async () => {
try {
await signOut()
// Redirect to your desired page
router.replace('/')
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2))
}
}
return (
<Pressable
style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
onPress={handleSignOut}
>
<ThemedText style={styles.buttonText}>Sign out</ThemedText>
</Pressable>
)
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#0a7ea4',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
buttonPressed: {
opacity: 0.7,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
})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 quickstart, 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.
To get started:
- Create a
(home)route group. - In the
(home)group, create a_layout.tsxfile with the following code. The useAuth() hook is used to check authentication state. Signed-out users are redirected to the sign-in page.
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 with the following code. If the user is signed in, it displays their email and the sign-out button you created in the previous step. If they're not signed in, it displays sign-in and sign-up links.
import { SignOutButton } from '@/components/sign-out-button'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { Show, useSession, useUser } from '@clerk/expo'
import { Link } from 'expo-router'
import { StyleSheet } from 'react-native'
export default function Page() {
const { user } = useUser()
// If your user isn't appearing as signed in,
// it's possible they have session tasks to complete.
// Learn more: https://clerk.com/docs/guides/configure/session-tasks
const { session } = useSession()
console.log(session?.currentTask)
return (
<ThemedView style={styles.container}>
<ThemedText type="title">Welcome!</ThemedText>
<Show when="signed-out">
<Link href="/(auth)/sign-in">
<ThemedText>Sign in</ThemedText>
</Link>
<Link href="/(auth)/sign-up">
<ThemedText>Sign up</ThemedText>
</Link>
</Show>
<Show when="signed-in">
<ThemedText>Hello {user?.emailAddresses[0].emailAddress}</ThemedText>
<SignOutButton />
</Show>
</ThemedView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
gap: 16,
},
})Run your project
Run your project with the following command:
npm startpnpm startyarn startbun startCreate your first user
- Visit your app's homepage at http://localhost:8081.
- Select "Sign up" on the page and authenticate to create your first user.
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 how to build custom flows for your native apps 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.
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