Skip to main content
Articles

Clerk Compatibility in Expo 54 and 55 - Part 2

Author: Roy Anger
Published: (last updated )

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 a mode prop: "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/expo plugin in app.json

Plugin options in app.json:

  • appleSignIn (boolean, default true): Enable Apple Sign-In entitlement
  • keychainService (string): Custom keychain service identifier
  • theme (string): Path to a JSON file for visual customization

Component props and behavior:

  • <AuthView /> accepts mode ("signIn", "signUp", or "signInOrUp" — the default), isDismissible (default true; set false to require the user to finish authenticating before the view can close), and onDismiss.
  • <UserProfileView /> accepts isDismissible (default true), onDismiss, and style. 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(), and useSession() hooks. Session state is automatically synced between the native SDK and the JavaScript SDK.

Important

When using native components with route guards, pass { treatPendingAsSignedOut: false } to useAuth() to prevent session desynchronization during initialization.

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() throws ClerkOfflineError (instead of returning null as in Core 2)
  • Not signed in: getToken() returns null

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 organization
  • useOrganizationList() — Access all organizations the user belongs to. Note: userMemberships, userInvitations, and userSuggestions are not populated by default — pass true or 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:

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>
  )
}

Note

System permissions are not included in session token claims. Use custom permissions for client-side authorization checks.

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-store

Additional dependencies based on which features you use:

FeaturePackage
Social OAuth / SSOexpo-auth-session, expo-web-browser
Native Google Sign-Inexpo-crypto
Native Apple Sign-Inexpo-apple-authentication, expo-crypto
Development buildsexpo-dev-client
Biometric sign-inexpo-local-authentication
Passkeys@clerk/expo-passkeys

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-client

ClerkProvider 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-here

Configure 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

Important

<Show> only conditionally renders its children on the client. The component code and data remain in the JavaScript bundle. For true route protection, use the strategies described in Route Protection with Expo Router.

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.

Caution

Never use the deprecated auth.expo.io proxy for OAuth redirects.

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.

Note

When using native components, pass { treatPendingAsSignedOut: false } to useAuth() in route guards to prevent premature redirects during session initialization.

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_KEY and 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

Note

The passkey peer-dependency gap and the GitHub issues in this section were reported against early @clerk/expo v3.1.x releases and, as of June 2026, have been resolved in later releases. They are retained as references for anyone pinned to an older version. Always confirm current status against the @clerk/expo GitHub issues tracker and upgrade to the latest release.

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().isLoaded stuck at false in 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 with jsi: true) blocking the JavaScript thread at startup, which prevented ClerkJS's Frontend API callbacks from resolving. If isLoaded never turns true, audit for startup work that blocks the JS thread (for WatermelonDB, set jsi: 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/expo release after v3.3.2.
  • #8149: getToken({ template }) throwing clerk_offline while getToken() 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.

FeatureExpo GoDev BuildExpo 54Expo 55
Email/password sign-in
Phone verification (OTP)
Magic links
Social OAuth (useSSO)
Native Google Sign-In
Native Apple Sign-IniOS only
Native components (beta)
Passkeys
Biometric sign-in
Token caching (expo-secure-store)
Offline support (experimental)
Session management hooks
<Show> component
Organizations / RBAC
<UserProfileView />
Legacy Architecture
New ArchitectureRequired

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

  1. Clerk Compatibility in Expo 54 and 55
  2. Clerk Compatibility in Expo 54 and 55 - Part 2 (you are here)