Skip to main content
Articles

Clerk Compatibility in Expo 54 and 55

Author: Roy Anger
Published:

@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.

Note

Current SDK version: @clerk/expo v3.x is part of Clerk Core 3, released March 3, 2026. The package was renamed from @clerk/clerk-expo to @clerk/expo. publishableKey is now a required prop on <ClerkProvider>. The <Show> component replaces <SignedIn>, <SignedOut>, and <Protect>. Hooks were reorganized into subpath imports (e.g., @clerk/expo/apple, @clerk/expo/google). This article covers the current SDK version and is not a migration guide. If upgrading from v2.x, refer to the Core 3 upgrade guide.

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.

ComponentExpo SDK 54Expo SDK 55
@clerk/expo version3.1.x3.1.x
React Native0.810.83
React19.119.2
New ArchitectureSupported (optional)Required (mandatory)
Legacy ArchitectureSupported (final SDK)Not available
Minimum Xcode16.1 (26 recommended)26
EAS Build default Xcode26.026.2
Android targetSdkVersion36 (API 36)36 (API 36)
Node.js>=20.9.0>=20.9.0

Note

Xcode versioning: Apple adopted year-based versioning at WWDC 2025, jumping from Xcode 16 directly to Xcode 26. Versions 17 through 25 do not exist. Xcode 26.0 GA shipped September 15, 2025, and Xcode 26.4 is the current stable release as of April 2026.

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 newArchEnabled configuration option has been removed.
  • Passkeys are supported on SDK 54, but @clerk/expo-passkeys does 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-launcher with 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.

Tip

If your project is still using the Legacy Architecture, SDK 54 is the last opportunity to upgrade to the New Architecture before SDK 55 makes it mandatory. Expo recommends upgrading to the New Architecture on SDK 54 first, testing, then upgrading the SDK version — rather than doing both simultaneously.

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: true and buildReactNativeFromSource: true in the expo-build-properties plugin, and overriding the hermes-compiler version in package.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)

Important

Legacy Architecture transition: As of April 2026, Expo Go in the App Store and Play Store remains on SDK 54. To use SDK 55 with Expo Go, install it via Expo CLI on Android, the TestFlight External Beta for iOS, or build a custom Expo Go via eas go. Additionally, npx create-expo-app defaults to SDK 54 during the transition period. To create an SDK 55 project, use npx create-expo-app@latest --template default@sdk-55.

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

ScenarioRecommended Environment
Prototyping with email/passwordExpo Go
Testing basic Clerk integrationExpo Go
Social login (Google, GitHub, etc.)Development Build
Native Google or Apple Sign-InDevelopment Build
Using native componentsDevelopment Build
Production deploymentDevelopment Build
Passkeys or biometric authDevelopment Build

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.

Note

Expo Go is not recommended for production apps. This is Expo's own guidance.

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:

  1. A development build (does not work in Expo Go)
  2. Clerk Dashboard: Register your native app — Team ID + Bundle ID for iOS, package name + SHA-256 fingerprint for Android
  3. 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)
  4. 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)
  5. Add the @clerk/expo plugin to app.json (see Plugin Configuration)
  6. Install peer dependency: expo-crypto

Warning

Android SHA-256 fingerprint mismatch is a common pitfall. Three different fingerprints exist: the debug keystore fingerprint, the EAS managed keystore fingerprint (get it via eas credentials -p android), and the Google Play App Signing key fingerprint (find it in Release > Setup > App Signing in the Play Console). Register the correct fingerprint for each environment in the Clerk Dashboard.

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:

  1. A development build
  2. Install peer dependencies: expo-apple-authentication, expo-crypto
  3. 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 SSO
  • identifier: Required for enterprise SSO to identify the connection
  • redirectUrl: Generated via AuthSession.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

Warning

Expo SDK 55 limitation: @clerk/expo-passkeys v1.0.13 declares a peer dependency of expo: >=53 <55, which does not include Expo SDK 55. No updated version or timeline has been announced as of April 2026. Installing with --legacy-peer-deps is a workaround but is not officially recommended. Check the npm package page for updated versions before relying on passkeys with SDK 55.

Important

Android emulators do not support passkeys. This is a platform-level limitation of Android's Credential Manager implementation, not a restriction from Clerk. Clerk's official documentation states: "Passkeys will not work with Android emulators. You must use a physical device."

iOS configuration requires adding associated domains to app.json:

  • applinks:<FAPI_URL> and webcredentials:<FAPI_URL>
  • Set ios.deploymentTarget: "16.0" via the expo-build-properties plugin
  • 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/expo v2.2.0 or later
  • expo-local-authentication v13.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 device
  • userOwnsCredentials: Whether the saved credentials belong to the current user
  • biometricType: 'face-recognition', 'fingerprint', or null
  • setCredentials(): Save the current user's password after initial sign-in
  • clearCredentials(): Remove saved credentials
  • authenticate(): 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 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

Additional hooks for native components:

  • useUserProfileModal(): Programmatically open the profile modal
  • useNativeSession(): Access native session state
  • useNativeAuthEvents(): Listen for native authentication events

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

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 with Clerk.

Known Issues and Limitations

Note

Issues listed in this section are transient and may be resolved by the time you read this article. Check the @clerk/expo GitHub issues tracker for current status.

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 an overrides field to package.json pinning @clerk/expo-passkeys to 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().isLoaded permanently false in 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/expo v3.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 }) throws clerk_offline error while getToken() 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.

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)
PasskeysPeer dep gap
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: @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.

Frequently Asked Questions