# Clerk Compatibility in Expo 54 and 55 - Part 2

> Part 2 of 2. Start with [Clerk Compatibility in Expo 54 and 55](https://clerk.com/articles/clerk-compatibility-in-expo-54-and-55.md).

**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](https://clerk.com/changelog/2026-03-09-expo-native-components.md) 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](https://clerk.com/glossary/multi-factor-authentication-mfa.md), 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.

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

```tsx
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](https://github.com/clerk/javascript/blob/main/packages/expo/CHANGELOG.md) (November 2025), so you no longer need to write the boilerplate manually.

Import `tokenCache` and pass it to `<ClerkProvider>`:

```tsx
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.

```tsx
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.

```typescript
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.

```typescript
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](https://clerk.com/glossary/user-profile.md) 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](https://clerk.com/glossary/organizations.md) enable [multi-tenant](https://clerk.com/glossary/multi-tenancy.md) 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](https://clerk.com/glossary/role-based-access-control-rbac.md) at the organization level:

- **Default [roles](https://clerk.com/glossary/roles.md):** Admin (`org:admin`) and Member (`org:member`)
- **[Custom roles](https://clerk.com/glossary/custom-roles.md):** [Up to 10 custom roles per instance](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions.md)
- **[Custom permissions](https://clerk.com/glossary/custom-permissions.md):** Format `org:<feature>:<permission>` (e.g., `org:billing:manage`)

Use the `has()` helper from `useAuth()` to check roles and permissions:

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

> **System permissions** are not included in [session token](https://clerk.com/glossary/customizable-session-tokens.md) claims. Use custom permissions for client-side [authorization](https://clerk.com/glossary/authorization.md) checks.

**Role Sets** ([launched January 2026](https://clerk.com/changelog/2026-01-12-organization-role-sets.md)) 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:

```bash
npx expo install @clerk/expo expo-secure-store
```

Additional dependencies based on which features you use:

| Feature               | Package                                    |
| --------------------- | ------------------------------------------ |
| Social OAuth / SSO    | `expo-auth-session`, `expo-web-browser`    |
| Native Google Sign-In | `expo-crypto`                              |
| Native Apple Sign-In  | `expo-apple-authentication`, `expo-crypto` |
| Development builds    | `expo-dev-client`                          |
| Biometric sign-in     | `expo-local-authentication`                |
| Passkeys              | `@clerk/expo-passkeys`                     |

Install all at once for a full-featured setup:

```bash
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](https://dashboard.clerk.com/~/api-keys):

```
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your-key-here
```

Configure the root layout (`app/_layout.tsx`):

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

> `<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](#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`:

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

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

```tsx
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:

```tsx
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.

> 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](https://docs.expo.dev/build-reference/infrastructure/)
- SDK 55: Defaults to Xcode 26.2 on EAS Build
- Use [EAS Secrets](https://docs.expo.dev/eas/) 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](https://clerk.com/glossary/cross-site-request-forgery-csrf.md) attacks in production.

For complete deployment instructions, see [Deploy an Expo app to production](https://clerk.com/docs/guides/development/deployment/expo.md).

## Known Issues and Limitations

> 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](https://github.com/clerk/javascript/issues?q=is%3Aopen+label%3Aexpo) 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](https://github.com/clerk/javascript/issues/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](https://github.com/clerk/javascript/issues/8265):** Session not restored after a Metro JS reload on Android. **Resolved** in [PR #8469](https://github.com/clerk/javascript/pull/8469) (closed June 1, 2026).
- **[#8288](https://github.com/clerk/javascript/issues/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](https://github.com/clerk/javascript/issues/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](https://github.com/clerk/javascript/pull/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.

| Feature                             | Expo Go | Dev Build | Expo 54 |  Expo 55 |
| ----------------------------------- | :-----: | :-------: | :-----: | :------: |
| Email/password sign-in              |   Yes   |    Yes    |   Yes   |    Yes   |
| Phone verification (OTP)            |   Yes   |    Yes    |   Yes   |    Yes   |
| Magic links                         |   Yes   |    Yes    |   Yes   |    Yes   |
| Social OAuth (`useSSO`)             |    No   |    Yes    |   Yes   |    Yes   |
| Native Google Sign-In               |    No   |    Yes    |   Yes   |    Yes   |
| Native Apple Sign-In                |    No   |  iOS only |   Yes   |    Yes   |
| Native components (beta)            |    No   |    Yes    |   Yes   |    Yes   |
| Passkeys                            |    No   |    Yes    |   Yes   |    Yes   |
| Biometric sign-in                   |    No   |    Yes    |   Yes   |    Yes   |
| Token caching (`expo-secure-store`) |   Yes   |    Yes    |   Yes   |    Yes   |
| Offline support (experimental)      |   Yes   |    Yes    |   Yes   |    Yes   |
| Session management hooks            |   Yes   |    Yes    |   Yes   |    Yes   |
| `<Show>` component                  |   Yes   |    Yes    |   Yes   |    Yes   |
| Organizations / RBAC                |   Yes   |    Yes    |   Yes   |    Yes   |
| `<UserProfileView />`               |    No   |    Yes    |   Yes   |    Yes   |
| Legacy Architecture                 |   N/A   |    Yes    |   Yes   |    No    |
| New Architecture                    |   N/A   |    Yes    |   Yes   | Required |

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

## FAQ

### How do I protect routes with Expo Router and Clerk?

Use a layout guard with `<Redirect>` or Expo Router's built-in `<Stack.Protected guard={!!isSignedIn}>`. Do not rely solely on the `<Show>` component, as it only conditionally renders content and does not prevent unauthenticated users from accessing the route.

### What is the difference between token caching on iOS and Android?

Clerk uses `expo-secure-store` for token caching. On iOS, it uses Apple Keychain, and data persists across app reinstalls if the bundle ID remains the same. On Android, it uses Keystore with encrypted SharedPreferences, and data is deleted when the app is uninstalled.

### Why do I need a custom URL scheme for my Expo app?

A custom URL scheme is required to redirect users back to your app after they complete a browser-based flow, such as OAuth social login or enterprise SSO. Without it, the browser cannot hand the session token back to the app.

## In this series

1. [Clerk Compatibility in Expo 54 and 55](https://clerk.com/articles/clerk-compatibility-in-expo-54-and-55.md)
2. **Clerk Compatibility in Expo 54 and 55 - Part 2** (you are here)
