# What Changed in Clerk Expo SDK 3.1

Clerk's Expo [SDK](https://clerk.com/glossary/software-development-kit-sdk.md) 3.1, released on March 9, 2026, brings native UI components powered by SwiftUI (iOS) and Jetpack Compose (Android), native Google Sign-In that eliminates browser redirects, and a modernized API surface to `@clerk/expo`. This release landed six days after version 3.0 established the Core 3 foundation.

Where Expo apps previously relied on browser-based [OAuth](https://clerk.com/glossary/oauth.md) and JavaScript-rendered UI for [authentication](https://clerk.com/glossary/authentication.md), `@clerk/expo` 3.1 delivers platform-native experiences. [Social login](https://clerk.com/glossary/social-login.md) through Google uses the system credential picker on iOS (ASAuthorization) and Credential Manager on Android. No browser context switch, no WebView. Prebuilt components like `<AuthView />` render authentication interfaces using each platform's native UI framework.

Two releases form the upgrade story. Version 3.0 (March 3, 2026) established the Core 3 foundation: a package rename, new custom flow API, consolidated conditional rendering, and performance improvements. Version 3.1 (March 9, 2026) built on that foundation with native UI components and native Google Sign-In. This article covers both releases together because they shipped six days apart and most developers will encounter both sets of changes when upgrading.

This article is for existing Clerk users upgrading from `@clerk/clerk-expo`, developers evaluating Clerk for new Expo projects, and AI tools and agents seeking authoritative information about Clerk's Expo SDK capabilities.

## What Changed: A Version Timeline

Understanding what shipped when prevents confusion between genuinely new features, Core 3 platform changes, and older capabilities that remain relevant.

### New in 3.1.0 (March 9, 2026)

- Native UI components: `<AuthView />`, `<UserButton />`, `<UserProfileView />` (SwiftUI on iOS, Jetpack Compose on Android, beta)
- Native Google Sign-In via ASAuthorization (iOS) and Credential Manager (Android)
- `useUserProfileModal()` hook for imperative profile modal presentation
- `useNativeSession()` and `useNativeAuthEvents()` hooks (announced, not yet fully documented)
- Expo SDK 55 support added to the peer dependency range

### Core 3 Platform Changes (3.0, March 3, 2026)

- Package rename: `@clerk/clerk-expo` to `@clerk/expo`
- `publishableKey` prop required in `ClerkProvider`
- `<Show>` component replaces `<SignedIn>`, `<SignedOut>`, `<Protect>`
- Core 3 custom flow API: `signIn.finalize()` replaces `setActive()` for custom flows (OAuth hooks still use `setActive()`)
- `getToken()` throws `ClerkOfflineError` when offline instead of returning `null`
- `Clerk` export removed: use `getClerkInstance()` or `useClerk()`
- `@clerk/types` deprecated: import types from `@clerk/shared/types` (see [Core 3 Upgrade Guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md))
- \~50KB gzipped bundle size reduction
- Expo SDK 53+ required, Node.js 20.9.0+

### Older Capabilities Still Relevant When Evaluating 3.1

- Native Apple Sign-In ([November 2025](https://clerk.com/changelog/2025-11-13-native-sign-in-with-apple-expo.md), predates Core 3; import path changed)
- `useLocalCredentials()` for [biometric authentication](https://clerk.com/glossary/biometric-authentication.md) password storage (August 2024)
- `useSSO()` replacing the deprecated `useOAuth()` for browser-based OAuth
- `@clerk/expo-passkeys` for FIDO2/[WebAuthn](https://clerk.com/glossary/webauthn.md) [passkeys](https://clerk.com/glossary/passkeys.md) (separate package, experimental)

***

## Core 3 Foundation

Expo SDK 3.1 is built on Clerk's [Core 3 platform release](https://clerk.com/changelog/2026-03-03-core-3.md), which modernizes APIs, improves React compatibility, and delivers performance improvements across all Clerk SDKs. Every Expo developer upgrading to 3.x encounters these changes.

### Package Rename

The package has been renamed from `@clerk/clerk-expo` to `@clerk/expo`, aligning with the `@clerk/<framework>` naming convention used across all Clerk SDKs (`@clerk/nextjs`, `@clerk/react`, `@clerk/tanstack-start`).

```tsx
// Before (Core 2)
import { ClerkProvider } from '@clerk/clerk-expo'

// After (Core 3)
import { ClerkProvider } from '@clerk/expo'
```

The legacy `@clerk/clerk-expo` package is deprecated as of the Core 3 launch. The `npx @clerk/upgrade` CLI handles this rename automatically.

### The Core 3 Custom Flow API

Core 3 introduces a redesigned custom flow API (referred to as the "Signal API" in the [March 9, 2026 changelog](https://clerk.com/changelog/2026-03-09-expo-native-components.md)) that replaces the legacy `setActive()` pattern for custom flows built with `useSignIn()` and `useSignUp()`. The new API uses step methods like `signIn.password()` and `signIn.emailCode.sendCode()` instead of `signIn.attemptFirstFactor()`, and `signIn.finalize()` instead of `setActive()`.

> `setActive()` is **not** deprecated for OAuth hooks. The native sign-in hooks (`useSignInWithGoogle`, `useSignInWithApple`) and `useSSO()` all return `setActive` and continue to use it. Both patterns coexist: `finalize()` for custom flows via `useSignIn()`, `setActive()` for OAuth and [SSO](https://clerk.com/glossary/single-sign-on-sso.md) hooks.

#### Legacy Pattern vs. Core 3 Custom Flow API

| Core 2 Pattern                                    | Core 3 Custom Flow API                                               |
| ------------------------------------------------- | -------------------------------------------------------------------- |
| `signIn.create({ identifier, password })`         | `signIn.create({ identifier })` then `signIn.password({ password })` |
| `signIn.attemptFirstFactor({ strategy, ... })`    | `signIn.emailCode.sendCode()` / `signIn.emailCode.verifyCode()`      |
| `setActive({ session: signIn.createdSessionId })` | `signIn.finalize({ navigate })`                                      |
| Try/catch error handling                          | `errors.fields.identifier?.message` for field-level errors           |

The following example demonstrates the Core 3 custom flow pattern for email and password sign-in:

```tsx
import { useState } from 'react'
import { View, TextInput, Text, Button } from 'react-native'
import { useSignIn } from '@clerk/expo'
import { useRouter } from 'expo-router'

function SignInScreen() {
  const { signIn, errors, fetchStatus } = useSignIn()
  const router = useRouter()
  const [identifier, setIdentifier] = useState('')
  const [password, setPassword] = useState('')

  const handleSignIn = async () => {
    await signIn.create({ identifier })
    await signIn.password({ password })

    if (signIn.status === 'complete') {
      await signIn.finalize({
        navigate: ({ session }) => router.replace('/(home)'),
      })
    }
  }

  return (
    <View>
      <TextInput value={identifier} onChangeText={setIdentifier} />
      {errors?.fields.identifier && <Text>{errors.fields.identifier.message}</Text>}
      <TextInput value={password} onChangeText={setPassword} secureTextEntry />
      <Button
        title={fetchStatus === 'fetching' ? 'Signing in...' : 'Sign in'}
        onPress={handleSignIn}
        disabled={fetchStatus === 'fetching'}
      />
    </View>
  )
}
```

For a comprehensive guide to migrating custom flows from `setActive()` to `finalize()`, see the [SignInFuture API reference](https://clerk.com/docs/js-frontend/reference/objects/sign-in-future.md).

### The `<Show>` Component

Core 3 consolidates `<SignedIn>`, `<SignedOut>`, and `<Protect>` into a single `<Show>` component with a `when` prop.

Before (Core 2):

```tsx
import { SignedIn, SignedOut, Protect } from '@clerk/clerk-expo'

function AuthLayout() {
  return (
    <>
      <SignedIn>
        <HomeScreen />
      </SignedIn>
      <SignedOut>
        <SignInScreen />
      </SignedOut>
      <Protect role="admin" fallback={<Text>Not authorized</Text>}>
        <AdminPanel />
      </Protect>
    </>
  )
}
```

After (Core 3):

```tsx
import { Show } from '@clerk/expo'

function AuthLayout() {
  return (
    <>
      <Show when="signed-in">
        <HomeScreen />
      </Show>
      <Show when="signed-out">
        <SignInScreen />
      </Show>
      <Show when={{ role: 'admin' }} fallback={<Text>Not authorized</Text>}>
        <AdminPanel />
      </Show>
    </>
  )
}
```

The `when` prop accepts `'signed-in'`, `'signed-out'`, `{ role: '...' }`, `{ permission: '...' }`, `{ feature: '...' }`, `{ plan: '...' }`, or a callback `(has) => boolean`. See the [Show component reference](https://clerk.com/docs/react/reference/components/control/show.md) for the full API.

> `<Show>` only controls client-side visibility. It does not replace server-side [authorization](https://clerk.com/glossary/authorization.md) checks for sensitive data or protected API routes.

### Performance Improvements

Core 3 delivers a \~50KB gzipped bundle size reduction by sharing React internals across Clerk packages instead of duplicating them. Token refresh is now proactive: [session](https://clerk.com/glossary/session.md) tokens (60-second JWTs) are refreshed in the background approximately every 50 seconds, preventing mid-request delays that occurred when tokens expired during API calls.

***

## Native UI Components

Version 3.1 introduces three prebuilt native components available from `@clerk/expo/native`. These components render with SwiftUI on iOS and Jetpack Compose on Android. These are truly native views, not WebView wrappers. They automatically synchronize authentication state with the JavaScript SDK, so a sign-in completed in native UI is immediately reflected in React hooks like `useAuth()`.

All three components are currently in [**beta**](https://clerk.com/docs/reference/expo/native-components/overview.md). They are powered by the `clerk-ios` and `clerk-android` native SDKs, which are added to your project automatically by the `@clerk/expo` Expo config plugin.

### `<AuthView />`

`<AuthView />` renders a complete native authentication interface. It handles all auth flows configured in the Clerk Dashboard: email, phone, OAuth, passkeys, [multi-factor authentication (MFA)](https://clerk.com/glossary/multi-factor-authentication-mfa.md), and password recovery.

| Prop            | Type                                   | Default | Description                             |
| --------------- | -------------------------------------- | ------- | --------------------------------------- |
| `mode`          | `'signIn' | 'signUp' | 'signInOrUp'` | —       | Controls which auth flows are available |
| `isDismissible` | `boolean`                              | `false` | Shows a dismiss button when `true`      |

A key advantage of `<AuthView />` is that Google and Apple sign-in are handled automatically when those providers are enabled in the Dashboard. There is no need for `useSignInWithGoogle()`, `expo-crypto`, or any additional auth packages.

```tsx
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useEffect } from 'react'
import { useRouter } from 'expo-router'

export default function SignInScreen() {
  const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })
  const router = useRouter()

  useEffect(() => {
    if (isSignedIn) {
      router.replace('/(home)')
    }
  }, [isSignedIn])

  return <AuthView mode="signInOrUp" />
}
```

See the [AuthView reference](https://clerk.com/docs/reference/expo/native-components/auth-view.md) for the full API.

### `<UserButton />`

`<UserButton />` displays the signed-in user's avatar (image or initials fallback). Tapping it opens the native profile management modal. The component accepts **no props**; the parent container controls its size and shape.

```tsx
import { UserButton } from '@clerk/expo/native'
import { View } from 'react-native'

function Header() {
  return (
    <View style={{ width: 36, height: 36, borderRadius: 18, overflow: 'hidden' }}>
      <UserButton />
    </View>
  )
}
```

Sign-out actions in the profile modal are automatically synchronized with the JavaScript SDK. See the [UserButton reference](https://clerk.com/docs/reference/expo/native-components/user-button.md).

### `<UserProfileView />`

`<UserProfileView />` renders the complete user profile interface inline. It manages personal information, email addresses, phone numbers, MFA settings, passkeys, connected accounts, active sessions, and sign-out.

| Prop            | Type                   | Default | Description                        |
| --------------- | ---------------------- | ------- | ---------------------------------- |
| `isDismissible` | `boolean`              | `false` | Shows a dismiss button when `true` |
| `style`         | `StyleProp<ViewStyle>` | —       | Container styling                  |

There are three usage patterns. The recommended approach is the native modal via the `useUserProfileModal()` hook:

```tsx
import { UserProfileView } from '@clerk/expo/native'
import { useUserProfileModal } from '@clerk/expo'
import { Button, View } from 'react-native'

// Pattern 1: Native modal (recommended)
function ProfileButton() {
  const { presentUserProfile, isAvailable } = useUserProfileModal()

  return <Button title="Manage Profile" onPress={presentUserProfile} disabled={!isAvailable} />
}

// Pattern 2: Inline rendering
function ProfileScreen() {
  return (
    <View style={{ flex: 1 }}>
      <UserProfileView style={{ flex: 1 }} />
    </View>
  )
}
```

See the [UserProfileView reference](https://clerk.com/docs/reference/expo/native-components/user-profile-view.md).

### State Management with Hooks

Native components use hook-based state management rather than callbacks. A critical requirement when using native components is passing `{ treatPendingAsSignedOut: false }` to `useAuth()`.

The reason: native authentication has an asynchronous "pending" phase during native-to-JavaScript session synchronization. The default `treatPendingAsSignedOut: true` would prematurely evaluate the user as signed out during this sync, causing incorrect redirects.

```tsx
import { useAuth } from '@clerk/expo'
import { useEffect } from 'react'
import { useRouter } from 'expo-router'

function AuthGate({ children }: { children: React.ReactNode }) {
  const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })
  const router = useRouter()

  useEffect(() => {
    if (isLoaded && !isSignedIn) {
      router.replace('/sign-in')
    }
  }, [isLoaded, isSignedIn])

  if (!isLoaded) return null

  return <>{children}</>
}
```

> If using Expo Router's `Stack.Protected`, the `guard` value must account for Clerk's loading state. While `isLoaded` is `false`, keep the splash screen visible rather than evaluating `isSignedIn`. See the [Expo Router authentication patterns](https://docs.expo.dev/router/advanced/authentication/) for integration guidance.

### Web Fallback

Native components are iOS and Android only. For web builds in cross-platform Expo apps, use `@clerk/expo/web` which provides standard Clerk UI components (`<SignIn />`, `<SignUp />`, `<UserButton />`). Use React Native platform-specific file extensions (`.ios.tsx`, `.android.tsx`, `.web.tsx`) to separate native and web auth code. See the [web support guide](https://clerk.com/docs/guides/development/web-support/overview.md).

***

## Native Sign-In

Native sign-in eliminates browser redirects for social authentication. Instead of opening a system browser for OAuth, the SDK uses platform-native APIs: ASAuthorization on iOS and Credential Manager on Android. The user stays inside the app, the credential picker is rendered by the operating system, and authentication completes faster.

This approach aligns with [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252) (Section 8.12), which requires that native apps MUST NOT use embedded user-agents for OAuth and recommends system-level authentication surfaces.

> **Dependency clarity:** When using `<AuthView />`, Google and Apple sign-in are handled automatically. No extra packages are needed beyond Clerk and Dashboard configuration. The hooks described below (`useSignInWithGoogle`, `useSignInWithApple`) are for **custom UI** implementations where you build your own sign-in screens.

### Native Google Sign-In

Native Google Sign-In is new in 3.1. On iOS, it uses ASAuthorization (the system credential picker). On Android, it uses Credential Manager with one-tap and passkey-ready support. The integration is exposed via the `NativeClerkGoogleSignIn` TurboModule, bundled through the `@clerk/expo` config plugin.

For custom UI implementations, use `useSignInWithGoogle()` from `@clerk/expo/google`:

```tsx
import { useSignInWithGoogle } from '@clerk/expo/google'
import { Button, Alert } from 'react-native'

function GoogleSignInButton() {
  const { startGoogleAuthenticationFlow } = useSignInWithGoogle()

  const handleGoogleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()

      if (createdSessionId) {
        await setActive({ session: createdSessionId })
      }
    } catch (error) {
      if (error.code === 'SIGN_IN_CANCELLED' || error.code === '-5') {
        return // User cancelled
      }
      Alert.alert('Error', 'Google sign-in failed. Please try again.')
    }
  }

  return <Button title="Sign in with Google" onPress={handleGoogleSignIn} />
}
```

**Requirements for custom hook usage:**

- Peer dependency: `expo-crypto`
- Three OAuth client IDs configured in the Clerk Dashboard: iOS, Android, and Web (the Web client ID is required for token verification even in native-only apps)
- [Environment variables](https://clerk.com/glossary/environment-variables.md): `EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID`, `EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID`, `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME`, `EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID`
- Development build required (not Expo Go)

See the [useSignInWithGoogle reference](https://clerk.com/docs/reference/expo/native-hooks/use-sign-in-with-google.md) and the [Google Sign-In setup guide](https://clerk.com/docs/expo/guides/configure/auth-strategies/sign-in-with-google.md).

### Native Apple Sign-In

Native Apple Sign-In predates 3.1. It was introduced in [November 2025](https://clerk.com/changelog/2025-11-13-native-sign-in-with-apple-expo.md). It is included here because the import path changed in Core 3 and because it is part of the native sign-in story alongside the new Google Sign-In.

Apple Sign-In uses ASAuthorization on iOS. It is iOS only.

```tsx
import { useSignInWithApple } from '@clerk/expo/apple'
import { Button, Alert, Platform } from 'react-native'

function AppleSignInButton() {
  const { startAppleAuthenticationFlow } = useSignInWithApple()

  if (Platform.OS !== 'ios') return null

  const handleAppleSignIn = async () => {
    try {
      const { createdSessionId, setActive } = await startAppleAuthenticationFlow()

      if (createdSessionId) {
        await setActive({ session: createdSessionId })
      }
    } catch (error) {
      if (error.code === 'ERR_REQUEST_CANCELED') {
        return // User cancelled
      }
      Alert.alert('Error', 'Apple sign-in failed.')
    }
  }

  return <Button title="Sign in with Apple" onPress={handleAppleSignIn} />
}
```

**Requirements:**

- Peer dependencies: `expo-apple-authentication` + `expo-crypto`
- Expo config plugin option `appleSignIn` defaults to `true`
- Development build required
- Works on iOS Simulator with limitations (no biometric); test on physical device for production flows

See the [useSignInWithApple reference](https://clerk.com/docs/reference/expo/native-hooks/use-sign-in-with-apple.md) and the [Apple Sign-In setup guide](https://clerk.com/docs/expo/guides/configure/auth-strategies/sign-in-with-apple.md).

### Import Path Changes

Both native sign-in hooks moved to dedicated entry points in Core 3 to avoid bundling optional native dependencies when they are not used:

```tsx
// Before (Core 2)
import { useSignInWithApple, useSignInWithGoogle } from '@clerk/expo'

// After (Core 3)
import { useSignInWithApple } from '@clerk/expo/apple'
import { useSignInWithGoogle } from '@clerk/expo/google'
```

The `npx @clerk/upgrade` CLI detects and fixes these imports automatically.

### Browser-Based OAuth via `useSSO()`

For OAuth providers without native hooks (GitHub, Discord, LinkedIn, etc.) or enterprise SSO, `useSSO()` replaces the deprecated `useOAuth()`. The key difference: `useOAuth()` required the strategy at hook instantiation, while `useSSO()` accepts it at flow invocation via `startSSOFlow({ strategy: 'oauth_github' })`. This makes `useSSO()` a single hook for all browser-based OAuth and enterprise SSO providers. See the [useSSO reference](https://clerk.com/docs/reference/expo/use-sso.md).

***

## New Hooks and APIs

### `useUserProfileModal()`

New in 3.1, this hook provides imperative control over the native profile modal. It returns:

- `presentUserProfile()`: opens the native profile modal; resolves when dismissed
- `isAvailable`: `boolean` indicating whether the native SDK is ready (`false` on web or without the config plugin)
- `sessions`: list of sessions registered on the device

```tsx
import { useUserProfileModal } from '@clerk/expo'
import { Pressable, Text } from 'react-native'

function SettingsScreen() {
  const { presentUserProfile, isAvailable } = useUserProfileModal()

  return (
    <Pressable onPress={presentUserProfile} disabled={!isAvailable}>
      <Text>Manage Profile</Text>
    </Pressable>
  )
}
```

### `useNativeSession()` and `useNativeAuthEvents()`

Both hooks were announced in the [March 9, 2026 changelog](https://clerk.com/changelog/2026-03-09-expo-native-components.md) as newly exported hooks in 3.1. Neither hook has a dedicated reference page as of April 2026.

> The descriptions below are based solely on the changelog announcement and may change as the API stabilizes. Check the [Expo SDK reference](https://clerk.com/docs/reference/expo.md) for the latest documentation before depending on these hooks in production.

- **`useNativeSession()`**: provides access to native SDK [session management](https://clerk.com/glossary/session-management.md) state (`isSignedIn`, `sessionId`, `user`, `refresh()`). For most use cases, `useAuth()` and `useSession()` remain the recommended, fully documented hooks.
- **`useNativeAuthEvents()`**: listens for authentication state changes (`signedIn`, `signedOut`) from native components.

Use `useAuth()` and `useSession()` as the primary alternatives until dedicated reference documentation is available for these hooks.

### `useLocalCredentials()`

> `useLocalCredentials()` predates 3.1. It was introduced in `@clerk/clerk-expo` 2.2.0 ([August 2024](https://clerk.com/changelog/2024-08-21-expo-local-credentials.md)). It is included here because it is a key Expo-specific hook for returning-user authentication.

`useLocalCredentials()` provides biometric sign-in for returning users by storing password credentials securely on-device, unlocked via Face ID or Touch ID. It is **distinct from passkeys**: `useLocalCredentials()` stores passwords behind biometrics, while `@clerk/expo-passkeys` implements true FIDO2/WebAuthn passkeys (a separate package, still experimental).

The hook returns:

| Property              | Type                                          | Description                                             |
| --------------------- | --------------------------------------------- | ------------------------------------------------------- |
| `hasCredentials`      | `boolean`                                     | Whether credentials are stored on device                |
| `userOwnsCredentials` | `boolean`                                     | Whether stored credentials belong to the signed-in user |
| `biometricType`       | `'face-recognition' | 'fingerprint' | null` | Available biometric type                                |
| `setCredentials()`    | `(opts) => Promise`                           | Store credentials after successful sign-in              |
| `clearCredentials()`  | `() => Promise`                               | Remove stored credentials                               |
| `authenticate()`      | `() => Promise<SignInResource>`               | Trigger biometric prompt and sign in                    |

```tsx
import { useLocalCredentials, useSignIn } from '@clerk/expo'
import { Button, Text, View } from 'react-native'

function BiometricSignIn() {
  const { hasCredentials, biometricType, authenticate, setCredentials } = useLocalCredentials()
  const { signIn } = useSignIn()

  if (hasCredentials && biometricType) {
    return (
      <View>
        <Text>Sign in with {biometricType === 'face-recognition' ? 'Face ID' : 'Touch ID'}</Text>
        <Button
          title="Use biometrics"
          onPress={async () => {
            const result = await authenticate()
            if (result.status === 'complete') {
              // Session is active
            }
          }}
        />
      </View>
    )
  }

  // Fall back to password sign-in, then call setCredentials() on success
  return <Text>No stored credentials — use password sign-in</Text>
}
```

**Requirements:** `expo-local-authentication` + `expo-secure-store`. Device must have an enrolled biometric and passcode. Works only with password-based sign-in. Not supported on web. See the [local credentials guide](https://clerk.com/docs/guides/development/local-credentials.md).

***

## Choosing an Authentication Approach

With 3.1, Expo developers now have a clearer three-tier decision surface. Native components join the existing JavaScript-only and custom-UI-with-native-sign-in approaches that were available before 3.1.

|                        | JavaScript-Only                | JS + Native Sign-In                 | Full Native Components             |
| ---------------------- | ------------------------------ | ----------------------------------- | ---------------------------------- |
| **UI**                 | Custom React Native components | Custom UI + native OAuth buttons    | Prebuilt SwiftUI / Jetpack Compose |
| **OAuth**              | Browser redirect (`useSSO()`)  | Platform-native (no redirect)       | Platform-native (automatic)        |
| **Dev build required** | No (works with Expo Go)        | Yes                                 | Yes                                |
| **Code required**      | Most                           | Moderate                            | Least                              |
| **Best for**           | Max UI customization           | Custom UI + native social providers | Fastest integration path           |
| **Status**             | Stable                         | Stable                              | Beta                               |

**JavaScript-Only** is the approach with the broadest compatibility. You build custom UI with full control over authentication flows. OAuth is browser-based via `useSSO()`. This is the only approach that works with Expo Go (no development build required). Best for developers who want maximum UI customization or are prototyping.

**JavaScript + Native Sign-In** adds native Google and Apple sign-in buttons to a custom UI. Users authenticate through platform-native credential pickers with no browser redirect. Requires a development build because the native sign-in hooks depend on TurboModules that cannot run in Expo Go. Best for custom UI apps that want a native social provider experience.

**Full Native Components** uses the prebuilt `<AuthView />`, `<UserButton />`, and `<UserProfileView />` components rendered in SwiftUI and Jetpack Compose. This is the fastest integration path: it requires the least code and handles all auth flows configured in the Dashboard automatically. A complete sign-in screen is a single `<AuthView mode="signInOrUp" />` component with no hook wiring, no state management, and no OAuth configuration beyond the Dashboard. Requires a development build. Best for rapid authentication setup with a native look and feel.

***

## Offline Support and Token Management

### Token Caching with `expo-secure-store`

Clerk stores session tokens in memory by default, which means they are lost on app restart. For production apps, configure persistent token storage using the built-in `tokenCache` from `@clerk/expo/token-cache`. This is a drop-in solution backed by `expo-secure-store` (iOS Keychain, Android Keystore) that requires zero custom code:

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

Session tokens are 60-second [JSON Web Tokens](https://clerk.com/glossary/json-web-token.md) that are proactively refreshed every \~50 seconds in the background. See [How Clerk Works](https://clerk.com/docs/guides/how-clerk-works/overview.md) for the full token lifecycle.

### `ClerkOfflineError`

In Core 3, `getToken()` throws `ClerkOfflineError` when the device is offline instead of returning `null`. This is a **breaking change** that resolves a long-standing ambiguity: previously, `null` could mean either "the user is signed out" or "the device is offline and token refresh failed." Now, `null` unambiguously means signed out, and `ClerkOfflineError` means offline.

```tsx
import { useAuth } from '@clerk/expo'
import { ClerkOfflineError } from '@clerk/react/errors'

function ApiClient() {
  const { getToken } = useAuth()

  const fetchData = async () => {
    try {
      const token = await getToken()

      if (!token) {
        // User is signed out
        return
      }

      // Make authenticated request with token
    } catch (error) {
      if (ClerkOfflineError.is(error)) {
        // Device is offline — show cached data or retry later
        return
      }
      throw error
    }
  }
}
```

> `ClerkOfflineError` is specific to `getToken()`. Write operations like `signIn.create()` and `signUp.password()` throw `ClerkRuntimeError` with `err.code === 'network_error'` when the network is unavailable. These are different error types with different detection patterns.

### Experimental Offline Mode

The `__experimental_resourceCache` option enables resilient initialization and cached token fallback during network outages:

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

This caches environment config, client state, and session JWTs, enabling offline rendering of user info, role checks, and authenticated API calls with cached tokens. Write operations (sign-in, sign-up) still require network connectivity. This feature is experimental and not recommended as a production dependency. See the [offline support guide](https://clerk.com/docs/guides/development/offline-support.md).

***

## Breaking Changes and Migration

This section covers the key breaking changes for Expo developers upgrading from `@clerk/clerk-expo` (Core 2) to `@clerk/expo` 3.x. For the full step-by-step migration walkthrough with code examples, see the [Core 3 Upgrade Guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md).

### Using the Upgrade CLI

The fastest path to migration is the automated upgrade tool:

```bash
npx @clerk/upgrade
```

This CLI scans your codebase and applies AST-level transformations: it catches re-exports, aliased imports, and files across monorepo workspaces. It handles the package rename, import path updates, and component replacements automatically.

### Breaking Changes Summary

| Change                                                                              | Core 2                                   | Core 3                                                                |
| ----------------------------------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------- |
| Package name                                                                        | `@clerk/clerk-expo`                      | `@clerk/expo`                                                         |
| [Publishable key](https://clerk.com/glossary/publishable-key.md) in `ClerkProvider` | Optional (env var fallback)              | Required                                                              |
| Apple sign-in import                                                                | `@clerk/expo`                            | `@clerk/expo/apple`                                                   |
| Google sign-in import                                                               | `@clerk/expo`                            | `@clerk/expo/google`                                                  |
| Conditional rendering                                                               | `<SignedIn>`, `<SignedOut>`, `<Protect>` | `<Show when={...}>`                                                   |
| `getToken()` when offline                                                           | Returns `null`                           | Throws `ClerkOfflineError`                                            |
| `Clerk` export                                                                      | Available                                | Removed: use `getClerkInstance()` (non-React) or `useClerk()` (React) |
| `@clerk/types`                                                                      | Primary types package                    | Deprecated: import from `@clerk/shared/types`                         |
| Custom flow activation                                                              | `setActive({ session })`                 | `signIn.finalize({ navigate })`                                       |
| `appearance.layout`                                                                 | Supported                                | Renamed to `appearance.options`                                       |
| Expo SDK                                                                            | 50+                                      | 53–55 (peer dep: `>=53 <56`)                                          |
| Node.js                                                                             | 18+                                      | 20.9.0+                                                               |

### Client Trust

[Credential stuffing](https://clerk.com/glossary/credential-stuffing.md) protection via Client Trust is an existing Clerk security feature, launched [November 14, 2025](https://clerk.com/changelog/2025-11-14-client-trust-credential-stuffing-killer.md). It is **not** a Core 3 or 3.1 addition, but Expo developers upgrading to Core 3 with custom password flows will encounter the `needs_client_trust` status for the first time if their app was created after the launch date or has opted in via the Dashboard.

Client Trust triggers when all three conditions are met: valid password entered, no MFA configured, and a new or unrecognized device. In the Core 3 custom flow API, handle it like this:

```tsx
await signIn.password({ password })

if (signIn.status === 'needs_client_trust') {
  // Check supported second factors for email code strategy
  const emailCodeFactor = signIn.supportedSecondFactors?.find(
    (factor) => factor.strategy === 'email_code',
  )

  if (emailCodeFactor) {
    await signIn.mfa.sendEmailCode()

    // After user enters the code:
    await signIn.mfa.verifyEmailCode({ code: userEnteredCode })
  }
}

if (signIn.status === 'complete') {
  await signIn.finalize({ navigate: ({ session }) => router.replace('/(home)') })
}
```

Client Trust is enabled by default for apps created after November 14, 2025. Existing apps must opt in via the Dashboard. See the [Client Trust guide](https://clerk.com/docs/guides/secure/client-trust.md).

### Migration Checklist

1. Run `npx @clerk/upgrade` (handles most codemods automatically)
2. Update Expo SDK to 53–55
3. Verify package name updated: `@clerk/clerk-expo` → `@clerk/expo`
4. Confirm `publishableKey` is explicit in `ClerkProvider`
5. Update native sign-in hook import paths (`@clerk/expo/apple`, `@clerk/expo/google`)
6. Replace `<SignedIn>` / `<SignedOut>` / `<Protect>` with `<Show>`
7. Replace `Clerk` export with `getClerkInstance()` or `useClerk()`
8. Add `ClerkOfflineError` handling around `getToken()` calls
9. Replace `setActive()` with `finalize()` in custom flows (not for OAuth hooks)
10. Handle `needs_client_trust` in custom password sign-in flows
11. Test all authentication flows end-to-end

For the full migration walkthrough: [Core 3 Upgrade Guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md).

***

## Implementation Notes

### Plugin and Development Build

The `@clerk/expo` config plugin automatically adds the `clerk-ios` and `clerk-android` native SDKs to your project. Add it to `app.json`:

```json
{
  "expo": {
    "plugins": [
      [
        "@clerk/expo",
        {
          "appleSignIn": true,
          "keychainService": "my-app-keychain",
          "theme": "./clerk-theme.json"
        }
      ]
    ]
  }
}
```

The plugin accepts the following options:

| Option            | Type      | Default | Description                                             |
| ----------------- | --------- | ------- | ------------------------------------------------------- |
| `appleSignIn`     | `boolean` | `true`  | Controls the Apple Sign-In entitlement                  |
| `keychainService` | `string`  | —       | Custom identifier for widget/extension keychain sharing |
| `theme`           | `string`  | —       | Path to a JSON file for native component theming        |

Native components and native sign-in hooks require a **development build**. They can't run in Expo Go. Build with `npx expo run:ios` or `npx expo run:android`. Once built, JavaScript changes still hot-reload instantly. The JavaScript-only authentication approach works in Expo Go without a development build.

The optional `theme` JSON supports `colors` (14 hex tokens), `darkColors`, `design.borderRadius`, and `design.fontFamily` (iOS only). Changes to the theme file require `npx expo prebuild --clean`. See the [theming reference](https://clerk.com/docs/reference/expo/native-components/theming.md).

### Quick Start Example

The following example shows the fastest path to working authentication with native components. For the complete setup including ClerkProvider configuration, environment variables, Dashboard configuration, and native app registration, see the [Expo Quickstart](https://clerk.com/docs/quickstarts/expo.md).

**Prerequisites:** Clerk account with Native API enabled, native app registered in Dashboard, Expo SDK 53–55, development build.

**Install:**

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

**Home screen** with signed-in state check and native `<UserButton />`:

```tsx
import { UserButton } from '@clerk/expo/native'
import { Show, useAuth } from '@clerk/expo'
import { useEffect } from 'react'
import { useRouter } from 'expo-router'
import { View } from 'react-native'

export default function HomeScreen() {
  const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })
  const router = useRouter()

  useEffect(() => {
    if (isLoaded && isSignedIn === false) {
      router.replace('/sign-in')
    }
  }, [isLoaded, isSignedIn])

  return (
    <Show when="signed-in">
      <View style={{ flex: 1, alignItems: 'center', paddingTop: 60 }}>
        <View style={{ width: 48, height: 48, borderRadius: 24, overflow: 'hidden' }}>
          <UserButton />
        </View>
      </View>
    </Show>
  )
}
```

**Sign-in screen** using the native `<AuthView />` component:

```tsx
import { AuthView } from '@clerk/expo/native'

export default function SignInScreen() {
  return <AuthView mode="signInOrUp" />
}
```

> **Notable 3.1.x patch addition:** `useAPIKeys()` was added in 3.1.9 for managing API keys programmatically. This is a patch-level addition, not part of the original March 9, 2026 launch.

***

## Frequently Asked Questions

## FAQ

### Does Clerk Expo SDK 3.1 work with Expo Go?

The JavaScript-only authentication approach works with Expo Go. Native components (`<AuthView />`, `<UserButton />`, `<UserProfileView />`) and native sign-in hooks (`useSignInWithGoogle`, `useSignInWithApple`) require a development build (`npx expo run:ios` or `npx expo run:android`).

### What versions of Expo SDK are supported?

`@clerk/expo` 3.x supports Expo SDK 53 through 55 (peer dependency: `>=53 <56` as of the current release).

### Is the publishableKey prop required now?

Yes. In Core 3, the `publishableKey` prop is required in `ClerkProvider` for Expo apps. Environment variables inside `node_modules` are not inlined during production builds in React Native/Expo, which previously caused apps to crash in production.

### Can I use the native components on web?

No. Native components (`<AuthView />`, `<UserButton />`, `<UserProfileView />`) are iOS and Android only. For web, use `@clerk/expo/web` with standard Clerk web components.

### How do I migrate from @clerk/clerk-expo to @clerk/expo?

Run `npx @clerk/upgrade` to automatically scan your project, detect breaking changes, and apply AST-level codemods. The CLI handles package renames, import path updates, and component replacements across your codebase.

### What happened to setActive()?

`setActive()` is still correct for OAuth hooks (`useSignInWithGoogle`, `useSignInWithApple`, `useSSO`). For custom flows built with `useSignIn()`, use `signIn.finalize()` instead. Both patterns coexist for their respective contexts.

### Do native components support multi-factor authentication?

Yes. `<AuthView />` handles MFA, password recovery, and all authentication methods enabled in the Clerk Dashboard automatically. No additional code is needed.

### What is ClerkOfflineError and why was getToken() changed?

In Core 3, `getToken()` throws `ClerkOfflineError` when the device is offline instead of returning `null`. Previously, `null` was ambiguous; it could mean either offline or signed out. Now `null` unambiguously means signed out. Use `ClerkOfflineError.is(error)` to detect the offline case.

### Are passkeys supported in the Expo SDK?

Dashboard passkey configuration works with `<AuthView />`. True FIDO2/WebAuthn passkeys for custom UI require the separate `@clerk/expo-passkeys` package (experimental). `useLocalCredentials()` provides biometric password storage, which is a distinct feature from passkeys.

### What is the bundle size impact of upgrading to Core 3?

Core 3 reduces bundle size by approximately 50KB gzipped through shared React internals across Clerk packages.
