
Clerk Compatibility in Expo 54 and 55
@clerk/expo v3.1.x fully supports both Expo SDK 54 and Expo SDK 55 for iOS and Android. This article provides a comprehensive compatibility reference for developers integrating Clerk authentication into Expo apps, covering version requirements, authentication approaches, feature availability, setup configuration, and known limitations. All information reflects @clerk/expo v3.1.12, Expo SDK 54, and Expo SDK 55 as of April 2026.
Clerk and Expo Compatibility: Version Support Matrix
The @clerk/expo SDK v3.1.x is compatible with both Expo SDK 54 and Expo SDK 55. The following table summarizes the version requirements for each component.
The @clerk/expo package declares the following peer dependencies: expo: >=53 <56, react: ^18 || ^19, and react-native: >=0.73. The minimum Node.js requirement is 20.9.0.
All three Clerk authentication approaches work on both SDK versions. The primary differences between SDK 54 and SDK 55 are:
- SDK 54 supports both the Legacy Architecture and the New Architecture. It is the last SDK to support the Legacy Architecture.
- SDK 55 requires the New Architecture. The
newArchEnabledconfiguration option has been removed. - Passkeys are supported on SDK 54, but
@clerk/expo-passkeysdoes not formally support SDK 55 (peer dependency gap).
How Clerk Integrates with Expo
Architecture Overview
The @clerk/expo package builds on top of @clerk/react, which wraps ClerkJS — the core JavaScript SDK. When you add <ClerkProvider> to your Expo app, it initializes the authentication context and connects to Clerk's Frontend API (FAPI) using your publishable key.
Clerk uses a hybrid stateful and stateless session model. Each session is stored in Clerk's database and represented on the client as a short-lived JSON Web Token (JWT) with a 60-second expiry. The SDK automatically refreshes this token on a 50-second interval, ensuring uninterrupted access without manual token management.
In Expo apps, token persistence is handled via the tokenCache prop on <ClerkProvider>. Using expo-secure-store, tokens are encrypted and stored on the device — in Apple Keychain on iOS and Android Keystore on Android. Without tokenCache, tokens are stored in memory and lost when the app restarts.
The publishable key (environment variable EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY) identifies your application and encodes the FAPI URL. This key must be explicitly passed as a prop to <ClerkProvider> in Expo because React Native production builds do not inline environment variables the same way web bundlers do.
Three Approaches to Clerk Authentication in Expo
Clerk organizes Expo authentication into three approaches, each adding capability and requiring more native integration.
Approach 1: JavaScript Custom Flows
JavaScript custom flows use only JavaScript-based authentication with no native module dependencies. This includes email/password sign-in and sign-up, phone verification via OTP, magic links (email links), and passwordless login. The relevant hooks are useSignIn(), useSignUp(), useAuth(), useUser(), and useSession().
JavaScript custom flows work in both Expo Go and development builds. This approach is suitable for rapid prototyping and for providers that do not have native SDKs.
Approach 2: JavaScript + Native Sign-In Hooks
This approach adds native platform integration for specific authentication methods:
- Native Google Sign-In via
useSignInWithGoogle(from@clerk/expo/google) — uses the platform's system credential picker (ASAuthorization on iOS, Credential Manager on Android) without browser redirects - Native Apple Sign-In via
useSignInWithApple(from@clerk/expo/apple) — uses Apple's native authentication UI on iOS - SSO/OAuth via
useSSO()— browser-based social login supporting 31+ providers, requiring a custom URL scheme for redirects - Biometric authentication via
useLocalCredentials(from@clerk/expo/local-credentials) — stores password credentials with biometric unlock
Approach 2 requires development builds and does not work in Expo Go because Expo Go cannot load custom native modules or register custom URL schemes.
Approach 3: Native Components (Beta)
Native components render fully native UI using SwiftUI on iOS (via the clerk-ios SDK) and Jetpack Compose on Android (via the clerk-android SDK). Three components are available:
<AuthView />— complete sign-in/sign-up UI that automatically handles all authentication methods enabled in the Clerk Dashboard<UserButton />— avatar that opens a native profile modal on tap<UserProfileView />— inline profile management (email, phone, MFA, passkeys, sessions, connected accounts)
Native components were released in beta on March 9, 2026 as part of @clerk/expo v3.1.0. They require development builds and the @clerk/expo plugin in app.json. Approach 1 and Approach 2 remain the production-stable options.
Expo SDK 54 Compatibility
Expo 54 at a Glance
Expo SDK 54 was released on September 10, 2025. It ships React Native 0.81 and React 19.1.
SDK 54 is the last SDK version to support the Legacy Architecture. Both the Legacy Architecture and the New Architecture work in SDK 54. At the time of SDK 54's release, approximately 75% of projects on EAS Build were already using the New Architecture.
Key platform changes in SDK 54:
- Precompiled XCFrameworks for faster iOS builds — clean build times dropped from approximately 120 seconds to approximately 10 seconds on M4 Max hardware
- Rebuilt
expo-dev-launcherwith improved debugging capabilities - Android 16 / API 36 is the default
targetSdkVersion, making edge-to-edge display mandatory - Minimum Xcode 16.1 required (Xcode 26 recommended)
Clerk Feature Support on Expo 54
@clerk/expo v3.1.x provides full support for all three authentication approaches on Expo SDK 54:
- Approach 1 (JavaScript custom flows): Fully supported in both Expo Go and development builds
- Approach 2 (JavaScript + native hooks): Fully supported in development builds
- Approach 3 (Native components): Supported in beta in development builds
Both the Legacy Architecture and the New Architecture are compatible with @clerk/expo. The SDK v3.1.5 release added the -Xskip-metadata-version-check Kotlin compiler flag for SDK 54 and SDK 55 compatibility, and fixed an Android New Architecture codegen error related to the NativeClerkModule.
Token caching with expo-secure-store works as expected on SDK 54. Passkeys are supported via @clerk/expo-passkeys, which includes SDK 54 in its peer dependency range (expo: >=53 <55).
There are no known Clerk-specific caveats unique to SDK 54.
Expo SDK 55 Compatibility
Expo 55 at a Glance
Expo SDK 55 was released on February 25, 2026. It ships React Native 0.83 and React 19.2, which introduces the Activity component and the useEffectEvent hook.
The most significant change in SDK 55 is that the New Architecture is mandatory. The newArchEnabled configuration option has been removed and the Legacy Architecture is no longer available. Approximately 83% of SDK 54 projects on EAS Build were already using the New Architecture before SDK 55 shipped.
Other notable changes in SDK 55:
- Hermes v1 available as an opt-in JavaScript engine. Enable it by setting
useHermesV1: trueandbuildReactNativeFromSource: truein theexpo-build-propertiesplugin, and overriding thehermes-compilerversion inpackage.json. Hermes v1 offers meaningful performance improvements and better support for modern JavaScript features. Caveat: it requires building React Native from source, which significantly increases native build times. It is not yet recommended for Android in monorepo projects. - Bytecode diffing for OTA updates — approximately 75% smaller update downloads
- Minimum Xcode 26 required (see Xcode versioning note in the Version Support Matrix)
- Native Tabs API and Apple Zoom transitions for enhanced navigation
- All Expo SDK packages now use matching major version numbers (e.g.,
expo-camera@^55.0.0)
Clerk Feature Support on Expo 55
@clerk/expo v3.1.0 added explicit Expo SDK 55 support by updating its peer dependency to expo: >=53 <56. All three authentication approaches are confirmed working:
- Approach 1 (JavaScript custom flows): Fully supported
- Approach 2 (JavaScript + native hooks): Fully supported
- Approach 3 (Native components): Supported in beta
The New Architecture is fully compatible with @clerk/expo. TurboModules and the Fabric renderer work without issues.
Known limitation: @clerk/expo-passkeys v1.0.13 declares a peer dependency of expo: >=53 <55, which excludes Expo SDK 55. Passkeys are not formally supported on SDK 55. No updated version or timeline has been announced as of April 2026. See the Known Issues and Limitations section for details.
Several open GitHub issues affect SDK 55 users. See the Known Issues and Limitations section for current status.
New Architecture Impact on Clerk Authentication
The mandatory New Architecture in SDK 55 requires no action from Clerk users. The @clerk/expo package uses expo-modules-core for native module integration, which supports the New Architecture by default.
Clerk's native components use JSI-based TurboModules for JavaScript-to-native communication. The Fabric renderer is fully compatible — no rendering issues have been reported.
React Native 0.83 introduced the option to compile out Legacy Architecture code entirely by setting RCT_REMOVE_LEGACY_ARCH=1. This produces approximately 20% faster iOS builds and approximately 6% smaller app size. This optimization is compatible with Clerk.
TurboModules also improve performance for Clerk operations by lazy-loading native modules on demand rather than eagerly loading them at startup, which reduces cold-start memory usage.
Expo Go vs. Development Builds
What Works in Expo Go
Expo Go supports Approach 1 only — JavaScript custom flows. The following features work in Expo Go:
- Email/password sign-in and sign-up
- Phone verification (OTP)
- Magic links
- Session management:
useAuth(),useUser(),useSession() - Conditional rendering:
<Show when="signed-in">,<Show when="signed-out"> - Loading states:
<ClerkLoaded>,<ClerkLoading> - Token caching with
expo-secure-store - Organizations, RBAC, and user management hooks
The following features do not work in Expo Go: social OAuth (useSSO()), native Google Sign-In, native Apple Sign-In, native components, passkeys, and biometric sign-in. Expo Go cannot register custom URL schemes (required for OAuth redirects) and cannot load custom native modules.
What Requires a Development Build
A development build is required for:
- Social OAuth via
useSSO()— custom URL scheme redirect required - Native Google Sign-In via
useSignInWithGoogle - Native Apple Sign-In via
useSignInWithApple - Native components (
<AuthView />,<UserButton />,<UserProfileView />) - Passkeys via
@clerk/expo-passkeys - Biometric sign-in via
useLocalCredentials - Custom URL scheme registration for deep linking
Choosing the Right Environment
Start with Expo Go for initial setup and email/password flows. Switch to a development build when adding social or native authentication.
Two ways to create a development build:
- Local build (
npx expo run:ios/npx expo run:android): Compiles using locally installed Xcode (iOS, macOS only) or Android Studio. Best for rapid iteration — rebuilds only changed native code on subsequent runs. No EAS account required. Note: a paid Apple Developer Program membership ($99/year) is effectively required for Apple Sign-In entitlement configuration, Associated Domains (needed for passkeys), and App Store distribution. - EAS Build (
eas build --profile development): Builds on remote EAS servers with no local native tooling required. Can build iOS from Windows or Linux. Handles credential management (certificates, provisioning profiles) automatically. Best for team builds, CI/CD, and distribution.
A new development build is required whenever native configuration changes (adding a URL scheme, passkey support, native sign-in, or plugins). JavaScript-only changes load via the dev server without rebuilding.
Authentication Methods in Detail
Native Google Sign-In
Native Google Sign-In provides a platform-native credential picker on both iOS and Android, with no browser redirect:
- iOS: Uses ASAuthorization — the system credential picker that appears natively
- Android: Uses Credential Manager — a system bottom sheet with one-tap support
Prerequisites:
- A development build (does not work in Expo Go)
- Clerk Dashboard: Register your native app — Team ID + Bundle ID for iOS, package name + SHA-256 fingerprint for Android
- Google Cloud Console: Create OAuth 2.0 credentials — an iOS Client ID, an Android Client ID, and a Web Application Client ID (the web client is required even for native apps)
- Environment variables:
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID,EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID(iOS),EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME(iOS) - Add the
@clerk/expoplugin toapp.json(see Plugin Configuration) - Install peer dependency:
expo-crypto
import { useSignInWithGoogle } from '@clerk/expo/google'
import { Pressable, Text } from 'react-native'
export function GoogleSignInButton() {
const { startGoogleAuthenticationFlow } = useSignInWithGoogle()
const onPress = async () => {
try {
const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()
if (createdSessionId) {
await setActive({ session: createdSessionId })
}
} catch (err) {
console.error('Google sign-in error:', err)
}
}
return (
<Pressable onPress={onPress}>
<Text>Sign in with Google</Text>
</Pressable>
)
}For detailed setup instructions including Google Cloud Console configuration, see Configure native Google Sign-In for Expo.
Native Apple Sign-In
Native Apple Sign-In uses Apple's authentication UI on iOS. It is iOS only — the hook returns null on non-iOS platforms. If your app offers social sign-in alongside another provider (e.g., Google), Apple may require "Sign in with Apple" for App Store approval.
Native Apple Sign-In supports Apple's Hide My Email privacy feature automatically.
Prerequisites:
- A development build
- Install peer dependencies:
expo-apple-authentication,expo-crypto - Clerk Dashboard: Register App ID Prefix (Team ID) + Bundle ID
When using native components, Apple Sign-In is automatically available in <AuthView /> when Apple is enabled in the Clerk Dashboard. For a custom UI, use the useSignInWithApple hook:
import { useSignInWithApple } from '@clerk/expo/apple'
import { Platform, Pressable, Text } from 'react-native'
export function AppleSignInButton() {
const signInWithApple = useSignInWithApple()
if (!signInWithApple || Platform.OS !== 'ios') return null
const onPress = async () => {
try {
const { createdSessionId, setActive } = await signInWithApple.startAppleAuthenticationFlow()
if (createdSessionId) {
await setActive({ session: createdSessionId })
}
} catch (err) {
console.error('Apple sign-in error:', err)
}
}
return (
<Pressable onPress={onPress}>
<Text>Sign in with Apple</Text>
</Pressable>
)
}For detailed configuration, see Configure native Apple Sign-In for Expo.
Browser-Based SSO
useSSO() opens the system browser for OAuth and enterprise SSO flows. It replaces the deprecated useOAuth() hook — all new code should use useSSO().
The browser experience uses ASWebAuthenticationSession on iOS and Chrome Custom Tabs on Android. useSSO() supports 31+ social providers (Google, GitHub, Discord, LinkedIn, and more) as well as enterprise SSO protocols (SAML, OIDC, EASIE).
useSSO() requires a development build. Expo Go cannot register custom URL schemes, which are required for the post-authentication redirect back to the app.
Key parameters:
strategy:'oauth_<provider>'for social login, or'enterprise_sso'for enterprise SSOidentifier: Required for enterprise SSO to identify the connectionredirectUrl: Generated viaAuthSession.makeRedirectUri()
import { useSSO } from '@clerk/expo'
import * as AuthSession from 'expo-auth-session'
import * as WebBrowser from 'expo-web-browser'
import { Pressable, Text, Platform } from 'react-native'
import { useEffect } from 'react'
export function SSOSignInButton() {
const { startSSOFlow } = useSSO()
useEffect(() => {
// Warm up the browser on Android for faster opening
if (Platform.OS === 'android') {
void WebBrowser.warmUpAsync()
return () => {
void WebBrowser.coolDownAsync()
}
}
}, [])
const onPress = async () => {
try {
const { createdSessionId, setActive } = await startSSOFlow({
strategy: 'oauth_google',
redirectUrl: AuthSession.makeRedirectUri(),
})
if (createdSessionId) {
await setActive({ session: createdSessionId })
}
} catch (err) {
console.error('SSO error:', err)
}
}
return (
<Pressable onPress={onPress}>
<Text>Sign in with Google (Browser)</Text>
</Pressable>
)
}Browser-based SSO offers broader provider support than native sign-in but trades UX polish for compatibility — users see a browser redirect rather than a system credential picker. For Google and Apple, native sign-in hooks provide a more seamless experience.
Passkeys
Clerk supports native passkeys in Expo via the @clerk/expo-passkeys package. Passkeys use WebAuthn to provide phishing-resistant, passwordless authentication bound to the device and domain.
- Create a passkey:
user.createPasskey() - Sign in with a passkey:
signIn.authenticateWithPasskey()
Platform requirements:
- iOS 16+ with Associated Domains entitlement
- Android 9+ with a physical device (emulators are not supported)
- Maximum 10 passkeys per account, domain-locked
iOS configuration requires adding associated domains to app.json:
applinks:<FAPI_URL>andwebcredentials:<FAPI_URL>- Set
ios.deploymentTarget: "16.0"via theexpo-build-propertiesplugin - Register App ID Prefix + Bundle ID in the Clerk Dashboard under Native Applications
Android configuration requires intent filters with autoVerify: true in app.json, pointing to your Clerk FAPI domain. Clerk hosts the assetlinks.json file on the FAPI domain — configure it via the Clerk Dashboard (Native Applications > Android), not self-hosted. Register the package name and SHA-256 fingerprints for each build environment.
// In your root layout, configure ClerkProvider with passkeys
import { ClerkProvider } from '@clerk/expo'
import { passkeys } from '@clerk/expo-passkeys'
import { tokenCache } from '@clerk/expo/token-cache'
export default function RootLayout() {
return (
<ClerkProvider
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
tokenCache={tokenCache}
__experimental_passkeys={passkeys}
>
{/* Your app */}
</ClerkProvider>
)
}In a sign-in component, authenticate with a passkey:
import { useSignIn } from '@clerk/expo'
import { Pressable, Text } from 'react-native'
export function PasskeySignIn() {
const { signIn, setActive } = useSignIn()
const onPress = async () => {
try {
const result = await signIn!.authenticateWithPasskey()
await setActive({ session: result.createdSessionId })
} catch (err) {
console.error('Passkey sign-in error:', err)
}
}
return (
<Pressable onPress={onPress}>
<Text>Sign in with Passkey</Text>
</Pressable>
)
}Passkeys remain experimental, as indicated by the __experimental_passkeys prop name. For detailed configuration including iOS Associated Domains and Android intent filters, see Configure passkeys for Expo.
Biometric Sign-In
The useLocalCredentials hook from @clerk/expo/local-credentials enables returning users to sign in using Face ID, Touch ID, or fingerprint authentication. It works by storing the user's password credentials securely on the device after their initial sign-in, then retrieving and auto-submitting them after biometric verification.
Requirements:
@clerk/expov2.2.0 or laterexpo-local-authenticationv13.5.0 or later- Password-based sign-in strategy enabled (does not work with OAuth-only accounts)
- Device must have enrolled biometrics and a passcode
- Development build required
The hook provides these key properties and methods:
hasCredentials: Whether saved credentials exist on this deviceuserOwnsCredentials: Whether the saved credentials belong to the current userbiometricType:'face-recognition','fingerprint', ornullsetCredentials(): Save the current user's password after initial sign-inclearCredentials(): Remove saved credentialsauthenticate(): Trigger biometric prompt and sign in
Credentials are automatically deleted if the device passcode is removed.
import { useLocalCredentials } from '@clerk/expo/local-credentials'
import { useSignIn } from '@clerk/expo'
import { Pressable, Text } from 'react-native'
export function BiometricSignIn() {
const { hasCredentials, biometricType, authenticate, setCredentials } = useLocalCredentials()
const { signIn, setActive } = useSignIn()
if (hasCredentials && biometricType) {
return (
<Pressable
onPress={async () => {
try {
const { createdSessionId } = await authenticate()
if (createdSessionId) {
await setActive({ session: createdSessionId })
}
} catch (err) {
console.error('Biometric auth failed:', err)
}
}}
>
<Text>Sign in with {biometricType === 'face-recognition' ? 'Face ID' : 'Fingerprint'}</Text>
</Pressable>
)
}
// After a successful password sign-in, offer to save credentials
// by calling setCredentials() to store for future biometric sign-in
return null
}For complete setup instructions, see Configure biometric sign-in for Expo.
Native Components (Beta)
Clerk's native components were released in beta on March 9, 2026 as part of @clerk/expo v3.1.0. They render fully native UI — SwiftUI on iOS and Jetpack Compose on Android — and automatically handle all authentication methods enabled in the Clerk Dashboard.
Three components are available:
<AuthView />— Complete sign-in and sign-up UI. Accepts amodeprop:"signIn","signUp", or"signInOrUp".<UserButton />— Avatar that opens a native profile modal on tap. Fills its parent container.<UserProfileView />— Inline profile management for email, phone, MFA, passkeys, sessions, and connected accounts.
Requirements:
- Expo SDK 53 or later
- Development build
@clerk/expoplugin inapp.json
Plugin options in app.json:
appleSignIn(boolean, defaulttrue): Enable Apple Sign-In entitlementkeychainService(string): Custom keychain service identifiertheme(string): Path to a JSON file for visual customization
Additional hooks for native components:
useUserProfileModal(): Programmatically open the profile modaluseNativeSession(): Access native session stateuseNativeAuthEvents(): Listen for native authentication events
import { AuthView } from '@clerk/expo/native'
import { Show } from '@clerk/expo'
import { View } from 'react-native'
export function AuthScreen() {
return (
<Show when="signed-out">
<View style={{ flex: 1 }}>
<AuthView mode="signInOrUp" />
</View>
</Show>
)
}Known bugs fixed in v3.1.10: iOS OAuth failure from the forgot password screen, Android stuck on "Get help" after sign-out, and a white flash on iOS mount. Approach 1 and Approach 2 remain the production-stable alternatives.
Token Management and Secure Storage
Token Caching with expo-secure-store
By default, Clerk stores session tokens in memory. This means tokens are lost when the app restarts, requiring the user to sign in again. For production apps, use expo-secure-store to persist tokens in encrypted storage.
The @clerk/expo/token-cache subpath provides a built-in wrapper around expo-secure-store. This was introduced in @clerk/expo v2.19.0 (November 2025), so you no longer need to write the boilerplate manually.
Import tokenCache and pass it to <ClerkProvider>:
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
tokenCache={tokenCache}
>
<Slot />
</ClerkProvider>
)
}Platform behavior differences:
- iOS: Uses Apple Keychain. Data persists across app reinstalls if the bundle ID remains the same.
- Android: Uses Keystore with encrypted SharedPreferences. Data is deleted on app uninstall.
Some iOS releases enforce an approximately 2,048-byte limit per Keychain item. Clerk's session tokens (60-second expiry JWTs) are well within this limit.
Experimental Offline Support
Clerk provides experimental offline support via the __experimental_resourceCache prop on <ClerkProvider>. This caches Clerk resources to secure storage and returns cached tokens when the network is unavailable.
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { resourceCache } from '@clerk/expo/resource-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
return (
<ClerkProvider
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}
tokenCache={tokenCache}
__experimental_resourceCache={resourceCache}
>
<Slot />
</ClerkProvider>
)
}In Core 3, getToken() behavior changed for offline scenarios:
- Offline:
getToken()throwsClerkOfflineError(instead of returningnullas in Core 2) - Not signed in:
getToken()returnsnull
Clerk exposes two different error types for offline scenarios, depending on the context:
Token retrieval (getToken()): Use ClerkOfflineError.is(error) with code 'clerk_offline'. This is thrown when getToken() fails after exhausting retries while offline.
import { ClerkOfflineError } from '@clerk/react/errors'
try {
const token = await getToken()
} catch (error) {
if (ClerkOfflineError.is(error)) {
// error.code === 'clerk_offline'
// Use cached data or show offline UI
}
}Custom flows (signIn.create(), signUp.create(), etc.): Use isClerkRuntimeError(err) with code === 'network_error'. When __experimental_resourceCache is set on <ClerkProvider>, it automatically enables experimental.rethrowOfflineNetworkErrors, which surfaces network errors from custom authentication flows as catchable ClerkRuntimeError instances.
import { isClerkRuntimeError } from '@clerk/expo'
try {
await signIn.create({ strategy: 'password', identifier, password })
} catch (err) {
if (isClerkRuntimeError(err) && err.code === 'network_error') {
// Network request failed — show offline UI or retry
}
}This feature is experimental and subject to change. It requires expo-secure-store.
User Management and Organizations
User Profile Access
The useUser() hook provides access to the current user's data — name, email addresses, phone numbers, profile image, and metadata. Use it to read and update user profile information in your Expo app.
Clerk provides three metadata tiers:
- Public metadata: Readable from the frontend and backend. Set from the backend only.
- Private metadata: Backend-only. Never exposed to the client.
- Unsafe metadata: Client-writable. Suitable for non-sensitive user preferences.
For a native profile management UI, the <UserProfileView /> component (beta) provides self-service profile management including email, phone, MFA configuration, passkeys, active sessions, and connected accounts.
Organizations and Multi-Tenant Support
Clerk Organizations enable multi-tenant functionality in Expo apps. The following hooks are available:
useOrganization()— Access and manage the currently active organizationuseOrganizationList()— Access all organizations the user belongs to. Note:userMemberships,userInvitations, anduserSuggestionsare not populated by default — passtrueor a configuration object to load them.useOrganizationCreationDefaults()— Suggested name and slug for new organizations
Switch between organizations programmatically via setActive({ organization: orgId }) from the useClerk() hook. Create organizations via createOrganization().
All organization hooks work identically in Expo as they do in web applications — they are the same React hooks from @clerk/react.
Roles and Permissions
Clerk provides role-based access control at the organization level:
- Default roles: Admin (
org:admin) and Member (org:member) - Custom roles: Up to 10 custom roles per instance
- Custom permissions: Format
org:<feature>:<permission>(e.g.,org:billing:manage)
Use the has() helper from useAuth() to check roles and permissions:
import { Show } from '@clerk/expo'
import { Text, View } from 'react-native'
export function TeamSettings() {
return (
<View>
<Show when={{ permission: 'org:team_settings:manage' }}>
<Text>Team Settings Panel</Text>
{/* Team management UI */}
</Show>
<Show when={{ role: 'org:admin' }}>
<Text>Admin-Only Actions</Text>
{/* Admin controls */}
</Show>
<Show
when={{ permission: 'org:billing:manage' }}
fallback={<Text>Contact your admin for billing access.</Text>}
>
<Text>Billing Management</Text>
</Show>
</View>
)
}Role Sets (launched January 2026) are collections of roles assigned per organization. The Primary Role Set is free. Additional Role Sets require the Enhanced B2B Authentication add-on. Changes to a Role Set propagate automatically to all organizations using it.
Without an active organization set, all authorization checks via has() return false.
Setting Up ClerkProvider for Expo
Required Dependencies
Install the core dependencies using npx expo install to ensure SDK-compatible versions:
npx expo install @clerk/expo expo-secure-storeAdditional dependencies based on which features you use:
Install all at once for a full-featured setup:
npx expo install @clerk/expo expo-secure-store expo-auth-session expo-web-browser expo-crypto expo-apple-authentication expo-dev-clientClerkProvider Configuration
The <ClerkProvider> component must wrap your entire Expo app. The publishableKey prop is required in Core 3.
Create a .env file with your publishable key from the Clerk Dashboard:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your-key-hereConfigure the root layout (app/_layout.tsx):
import { ClerkProvider, ClerkLoaded } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
export default function RootLayout() {
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
throw new Error('EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is not set')
}
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
<ClerkLoaded>
<Slot />
</ClerkLoaded>
</ClerkProvider>
)
}The <Show> component conditionally renders content based on authentication state:
<Show when="signed-in">— Visible only to authenticated users<Show when="signed-out">— Visible only to unauthenticated users<Show when={{ role: 'org:admin' }}>— Visible only to users with a specific role<Show when={{ permission: 'org:billing:manage' }}>— Visible only to users with a specific permission
URL Scheme and Expo Plugin Configuration
URL Scheme (required for OAuth/SSO):
The scheme property in app.json is required for useSSO() and any browser-based OAuth flow. Without it, OAuth redirects complete but cannot pass information back to the app — the user must manually dismiss the browser.
Expo Plugin (required for Approach 2 and Approach 3):
Add both the scheme and the @clerk/expo plugin to your app.json:
{
"expo": {
"scheme": "your-app-scheme",
"plugins": [
[
"@clerk/expo",
{
"appleSignIn": true
}
]
]
}
}The @clerk/expo plugin automatically adds:
- iOS: clerk-ios SDK, URL scheme for native Google Sign-In
- Android: clerk-android SDK, Credential Manager support
The plugin is not needed if you are only using Approach 1 (JavaScript custom flows).
After changing scheme or plugin configuration, rebuild your development build. The redirect URL (e.g., your-app-scheme://callback) must be allowlisted in the Clerk Dashboard.
Route Protection with Expo Router
The <Show> component is for conditional UI rendering only — it does not protect routes. Three strategies are available for true route protection. All work identically on SDK 54 and SDK 55.
Strategy 1: Layout guard with <Redirect> (Clerk's documented pattern)
Use useAuth() in a route group's _layout.tsx and return <Redirect> before rendering <Stack> for unauthenticated users:
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'
export default function ProtectedLayout() {
const { isLoaded, isSignedIn } = useAuth()
if (!isLoaded) return null
if (!isSignedIn) {
return <Redirect href="/(auth)/sign-in" />
}
return <Stack />
}This prevents child routes from ever mounting for unauthenticated users.
Strategy 2: Stack.Protected with guard (Expo Router built-in)
Available since Expo Router v5 (SDK 53), Stack.Protected provides declarative access control with automatic redirect to the nearest accessible screen:
import { useAuth } from '@clerk/expo'
import { Stack } from 'expo-router'
export default function AppLayout() {
const { isSignedIn } = useAuth()
return (
<Stack>
<Stack.Protected guard={!!isSignedIn}>
<Stack.Screen name="dashboard" />
<Stack.Screen name="profile" />
</Stack.Protected>
<Stack.Screen name="sign-in" />
</Stack>
)
}Stack.Protected automatically cleans up navigation history for newly-protected screens. It is also available as Tabs.Protected and Drawer.Protected.
Strategy 3: Programmatic redirect with useEffect is an alternative that uses useAuth() + useRouter() + useSegments() in a useEffect. This is less preferred because it runs after mount, causing a brief flash of protected content. Use it only when redirect logic depends on complex conditions beyond authentication state.
Use Strategy 1 (layout guard) as the primary approach — it is Clerk's documented pattern. Use Strategy 2 for apps that want Expo Router's built-in history cleanup.
Production Deployment Considerations
Moving from development to production requires several configuration changes.
Replace development credentials: Development instances use pk_test_ / sk_test_ keys. Production instances use pk_live_ / sk_live_ keys. Create a production instance in the Clerk Dashboard — SSO connections, integrations, and path settings do not transfer from development.
Register native apps in the Clerk Dashboard under Native Applications:
- iOS: App ID Prefix (Team ID) + Bundle ID
- Android: Package name + SHA-256 fingerprints
Allowlist redirect URLs for OAuth security. Default pattern: {bundleIdentifier}://callback.
Replace shared OAuth credentials: Development environments provide pre-configured social provider credentials. Production requires your own OAuth app credentials registered in each provider's dashboard (Google Cloud Console, Apple Developer, etc.).
EAS Build configuration:
- SDK 54: Defaults to Xcode 26.0 on EAS Build
- SDK 55: Defaults to Xcode 26.2 on EAS Build
- Use EAS Secrets for
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEYand other sensitive values
Domain configuration: A custom domain is mandatory for production instances, even for mobile-only apps.
authorizedParties configuration is recommended on your backend to prevent CSRF attacks in production.
For complete deployment instructions, see Deploy an Expo app with Clerk.
Known Issues and Limitations
Passkeys on Expo SDK 55
@clerk/expo-passkeys v1.0.13 declares a peer dependency of expo: >=53 <55. This formally excludes Expo SDK 55. No fix, pull request, or timeline has been announced as of April 2026. The underlying native passkey APIs (iOS Associated Domains, Android Credential Manager) did not change between SDK 54 and SDK 55, so the gap is a packaging constraint rather than a runtime incompatibility.
- Workaround (unofficial): Install with
--legacy-peer-deps, or add anoverridesfield topackage.jsonpinning@clerk/expo-passkeysto accept SDK 55. Neither is endorsed by Clerk and both bypass the peer dependency check without runtime guarantees. - Recommendation: Check the npm package page for version updates before relying on passkeys with SDK 55. If passkeys are a launch requirement, staying on SDK 54 until an updated release ships is the safer path.
Open GitHub Issues (as of April 2026)
- #8245:
useAuth().isLoadedpermanentlyfalsein real and monorepo apps on SDK 55. Works in fresh apps but fails in complex project structures. - #8265: Session lost after Metro JS reload on Android (
@clerk/expov3.1.6+ regression from v2). iOS is unaffected. - #8288:
useSSO()/useOAuth()dynamic import fails in monorepo and Bun setups under Metro. Silent error masking. - #8149:
getToken({ template })throwsclerk_offlineerror whilegetToken()without a template works. Blocks Convex integration. - PR #8303: Fix for background token refresh destroying sessions on iOS when the app is backgrounded (JavaScript event loop throttled).
Native Components Beta Limitations
<AuthView />, <UserButton />, and <UserProfileView /> are in beta. They are not available in Expo Go. Known bugs were fixed in v3.1.10 (iOS OAuth failure from forgot password screen, Android stuck state after sign-out, iOS white flash on mount). Approach 1 and Approach 2 are production-stable alternatives.
General Limitations
- Prebuilt web UI components (
<SignIn />,<SignUp />) are web-only and not available on native platforms. Use JavaScript custom flows (Approach 1) or native components (Approach 3) instead. useLocalCredentials()requires a password-based sign-in strategy. It does not work with OAuth-only accounts.- Android emulators do not support passkeys. A physical device is required. This is a platform-level Credential Manager limitation.
- iOS Keychain data persists across reinstalls. Android Keystore data is deleted on uninstall. This behavioral difference affects token persistence.
Compatibility Reference Table
The following table provides a comprehensive feature-by-feature compatibility matrix for Clerk with Expo.
Passkeys note: @clerk/expo-passkeys v1.0.13 peer dependency is expo: >=53 <55. Check npm for version updates.
Native Apple Sign-In note: Returns null on non-iOS platforms. iOS-only.