
Clerk Compatibility in Expo 54 and 55 - Part 2
Part 2 of 2. Start with Clerk Compatibility in Expo 54 and 55.
How do I configure Native Components and advanced authentication features in Expo 54 and 55?
Use the @clerk/expo/native beta components (<AuthView />, <UserButton />, and <UserProfileView />) alongside tokenCache for encrypted session persistence. Part 2 of this series details the beta Native Components, token caching with expo-secure-store, user and organization management, <ClerkProvider> setup, and production deployment considerations. For version requirements, architecture integration, and core authentication hooks, see Part 1.
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
Component props and behavior:
<AuthView />acceptsmode("signIn","signUp", or"signInOrUp"— the default),isDismissible(defaulttrue; setfalseto require the user to finish authenticating before the view can close), andonDismiss.<UserProfileView />acceptsisDismissible(defaulttrue),onDismiss, andstyle. There is no separate "open profile modal" hook — render<UserProfileView />inline (it fills its parent) inside your own modal, sheet, or route, or let<UserButton />open it on tap.- For session state and authentication events, use the standard
useAuth(),useUser(), anduseSession()hooks. Session state is automatically synced between the native SDK and the JavaScript SDK.
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 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
Adding the plugin also raises the minimum iOS deployment target to 17.0, which the bundled native iOS SDK requires. 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 to production.
Known Issues and Limitations
Passkeys on Expo SDK 55 (resolved)
Early @clerk/expo-passkeys releases (up to v1.0.13) capped their peer dependency at expo: >=53 <55, which formally excluded Expo SDK 55. This was resolved in @clerk/expo-passkeys v1.1.0 (May 2026), which widened the peer dependency to expo: >=53 <57 — formally including SDK 55. Upgrade to the latest @clerk/expo-passkeys release; no --legacy-peer-deps install or overrides pin is needed. Part 1 covers passkey configuration in full.
Resolved GitHub Issues
These issues were tracked against early v3.1.x releases and have since been resolved. They remain useful references if you are pinned to an older version:
- #8245:
useAuth().isLoadedstuck atfalsein complex apps on SDK 55. Resolved (closed May 3, 2026) — not a Clerk bug. The root cause was a third-party dependency (WatermelonDB's SQLite adapter withjsi: true) blocking the JavaScript thread at startup, which prevented ClerkJS's Frontend API callbacks from resolving. IfisLoadednever turnstrue, audit for startup work that blocks the JS thread (for WatermelonDB, setjsi: false). - #8265: Session not restored after a Metro JS reload on Android. Resolved in PR #8469 (closed June 1, 2026).
- #8288:
useSSO()/useOAuth()dynamic import failing under Metro in monorepo and Bun setups (masked by an empty catch). Resolved (closed June 3, 2026); the fix shipped in a@clerk/exporelease after v3.3.2. - #8149:
getToken({ template })throwingclerk_offlinewhilegetToken()without a template worked. Closed as not planned (auto-closed after inactivity, May 4, 2026); no code fix landed, so test JWT-template retrieval on your target version if you depend on it offline. - PR #8303: Fix preventing background token refresh from destroying sessions on mobile when the app is backgrounded. Merged April 17, 2026 and shipped in a later release.
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: Passkeys require a development build on all SDK versions. @clerk/expo-passkeys v1.1.0+ declares expo: >=53 <57, which includes SDK 55; upgrade to the latest release.
Native Apple Sign-In note: Returns null on non-iOS platforms. iOS-only.
Conclusion
This concludes our comprehensive guide to Clerk compatibility in Expo 54 and 55. For details on version support, Expo Go vs. development builds, and core authentication hooks, refer back to Part 1.
FAQ
In this series
- Clerk Compatibility in Expo 54 and 55
- Clerk Compatibility in Expo 54 and 55 - Part 2 (you are here)