evals/evals.json
{
"skill_name": "clerk-expo",
"evals": [
{
"id": 1,
"prompt": "add auth to my expo app with clerk",
"expected_output": "Defaults to prebuilt native components: ClerkProvider with tokenCache at the root, AuthView in a Modal kept mounted beside signed-in content, UserButton when signed in, dev build called out",
"expectations": [
"Recommends AuthView/UserButton from @clerk/expo/native as the default path and mentions they are in beta",
"Wraps app in ClerkProvider with publishableKey from process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY and tokenCache from @clerk/expo/token-cache",
"Registers the @clerk/expo config plugin (and expo-secure-store) in app.json",
"Passes { treatPendingAsSignedOut: false } to the useAuth() call gating the auth UI",
"States that a development build (expo run:ios / run:android) is required and Expo Go will not work",
"Does not call setActive() after native component auth and does not pair AuthView with useSignInWithGoogle/useSignInWithApple"
]
},
{
"id": 2,
"prompt": "add phone sms auth to my expo app with clerk",
"expected_output": "Custom flow using the current method-based API: signUp.create({ phoneNumber }) + verifications.sendPhoneCode/verifyPhoneCode, signIn.phoneCode.sendCode/verifyCode, finalize() on complete, with a note to enable phone/SMS in the Clerk Dashboard",
"expectations": [
"Verifies or instructs that phone number + SMS verification is enabled in the Clerk Dashboard before implementing",
"Uses signUp.create({ phoneNumber }) then signUp.verifications.sendPhoneCode() and verifyPhoneCode({ code }) for sign-up",
"Uses signIn.phoneCode.sendCode() and signIn.phoneCode.verifyCode({ code }) for sign-in",
"Calls finalize({ navigate }) when status is complete instead of setActive({ session })",
"Does NOT use the legacy API (prepareFirstFactor, attemptFirstFactor, isLoaded from the hook)",
"Renders <View nativeID=\"clerk-captcha\" /> on the sign-up screen",
"Shows the code-entry step only after a code was successfully sent (gated via signUp.status/unverifiedFields or explicit step state)"
]
},
{
"id": 3,
"prompt": "i want users to sign in with Google in my expo app using clerk",
"expected_output": "Uses useSSO with startSSOFlow({ strategy: 'oauth_google' }) for the browser flow (or the native useSignInWithGoogle hook with a dev build), calling setActive with createdSessionId and treating cancellation as non-fatal",
"expectations": [
"Uses useSSO (never the deprecated useOAuth) for the browser-based flow",
"Calls setActive({ session: createdSessionId }) after a successful flow",
"Does not call WebBrowser.maybeCompleteAuthSession() manually",
"Treats user cancellation (no createdSessionId) as non-fatal with no error UI",
"If offering native Google sign-in, imports useSignInWithGoogle from @clerk/expo/google, requires a dev build, and handles SIGN_IN_CANCELLED/-5 error codes",
"Mentions enabling the Google social connection in the Clerk Dashboard"
]
},
{
"id": 4,
"prompt": "my expo router app has a (tabs) group that should only be accessible to signed-in users. redirect unauthenticated users to the sign-in screen.",
"expected_output": "Layout-level guard using useAuth: checks isLoaded before isSignedIn, returns null while loading, uses expo-router Redirect",
"expectations": [
"Imports useAuth from @clerk/expo",
"Checks isLoaded before isSignedIn",
"Returns null or a loading indicator when isLoaded is false",
"Uses Expo Router's <Redirect> component (not router.push in an effect)",
"Places the guard in the group's _layout.tsx"
]
},
{
"id": 5,
"prompt": "build a custom email/password sign in screen for my expo app with clerk, i don't want the prebuilt UI",
"expected_output": "Custom flow with the current API: signIn.password({ emailAddress, password }), errors.fields for display, fetchStatus to disable the button, finalize on complete, MFA/Device Trust branches handled",
"expectations": [
"Destructures { signIn, errors, fetchStatus } from useSignIn()",
"Calls signIn.password({ emailAddress, password }) and checks the returned error",
"Calls signIn.finalize({ navigate }) when signIn.status === 'complete'",
"Handles needs_second_factor and/or needs_client_trust status branches",
"Surfaces field errors from errors.fields and disables submit while fetchStatus === 'fetching'",
"Does NOT use the legacy signIn.create + attemptFirstFactor + setActive pattern"
]
},
{
"id": 6,
"prompt": "i'm building an expo app and want the clerk session to persist after app restarts",
"expected_output": "Passes tokenCache from @clerk/expo/token-cache to ClerkProvider with the publishable key from EXPO_PUBLIC env",
"expectations": [
"Imports tokenCache from @clerk/expo/token-cache",
"Passes tokenCache prop to ClerkProvider",
"Reads the key from process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY (not NEXT_PUBLIC_)",
"Installs expo-secure-store via npx expo install",
"Does not use expo-secure-store directly or AsyncStorage for session tokens"
]
},
{
"id": 7,
"prompt": "my expo app supports organizations. i need a screen where users can see their organizations and switch the active one.",
"expected_output": "Uses useOrganizationList to list memberships and setActive, uses useOrganization to show the current org",
"expectations": [
"Imports useOrganization and useOrganizationList from @clerk/expo",
"Calls setActive from useOrganizationList to switch organizations",
"Lists available orgs from userMemberships.data",
"Shows the currently active organization name",
"Handles the case where userMemberships.data is undefined or loading"
]
},
{
"id": 8,
"prompt": "add 2fa with an authenticator app to my clerk expo sign in flow",
"expected_output": "Handles signIn.status === 'needs_second_factor' with signIn.mfa.verifyTOTP({ code }), then finalize",
"expectations": [
"Branches on signIn.status === 'needs_second_factor' after the first factor",
"Uses signIn.mfa.verifyTOTP({ code }) for the authenticator code",
"Calls finalize when status becomes complete",
"Optionally offers backup codes via signIn.mfa.verifyBackupCode",
"Does NOT use legacy attemptSecondFactor"
]
}
]
}
references/custom-flows.md
# Custom flows (@clerk/expo hooks)
Build your own auth UI with `useSignIn()` / `useSignUp()`. Works everywhere including Expo Go and web. Use when the developer wants their own UI, needs Expo Go, or asked for a specific strategy like SMS.
> Canonical docs — each has an Expo tab with a full working example; fetch the relevant one to re-verify if the installed SDK is newer than 3.6.x:
> - Email/password: https://clerk.com/docs/guides/development/custom-flows/authentication/email-password
> - Email/SMS OTP: https://clerk.com/docs/guides/development/custom-flows/authentication/email-sms-otp
> - Combined sign-in-or-up: https://clerk.com/docs/guides/development/custom-flows/authentication/sign-in-or-up
> - MFA: https://clerk.com/docs/guides/development/custom-flows/authentication/multi-factor-authentication
> - Forgot password: https://clerk.com/docs/guides/development/custom-flows/authentication/forgot-password
> - Email links: https://clerk.com/docs/guides/development/custom-flows/authentication/email-links
> - Error handling: https://clerk.com/docs/guides/development/custom-flows/error-handling
## The current API (v3.4+) — not the legacy one
`useSignIn()` and `useSignUp()` return `{ signIn, errors, fetchStatus }` / `{ signUp, errors, fetchStatus }`:
- **Method-based flows**: `signIn.password({...})`, `signIn.phoneCode.sendCode({...})`, `signUp.verifications.verifyEmailCode({...})`. Every method resolves to `{ error: ClerkError | null }` — check `error`, don't rely on try/catch for API errors.
- **`errors`** — reactive error state; field-level errors at `errors.fields.<name>` (e.g. `errors.fields.identifier`, `errors.fields.code`).
- **`fetchStatus`** — `'idle' | 'fetching'`; use it to disable submit buttons.
- **`signIn.status`** — `'needs_identifier' | 'needs_first_factor' | 'needs_second_factor' | 'needs_client_trust' | 'needs_new_password' | 'complete'`.
- **`signUp.status`** — `'missing_requirements' | 'complete'`, with `signUp.unverifiedFields` / `signUp.missingFields` saying what's left.
- **`finalize({ navigate })`** — converts a `complete` sign-in/up into the active session. Replaces `setActive({ session })`.
- **`reset()`** — clears the attempt so the user can start over (local-only, no API call).
Never generate the legacy shape for new code: `isLoaded`/`setActive` destructured from `useSignIn()`/`useSignUp()` (the current hooks don't return them — `isLoaded` from `useAuth()`/`useUser()` is fine), or `signIn.create()` chained with `prepareFirstFactor()`/`attemptFirstFactor()` + `setActive({ session: createdSessionId })`. That API lives at `@clerk/expo/legacy` and is only for maintaining code that already uses it. (`signIn.create()` still exists on the new resource but is for advanced cases — prefer the factor-specific methods.)
If the installed `@clerk/expo` is older than 3.4 and hooks don't have this shape, tell the developer and offer to upgrade rather than writing legacy code.
## Before writing flow code
1. **Check enabled factors** (SKILL.md Gate 3). Derive the Frontend API URL from the publishable key and fetch `<frontendApiUrl>/v1/environment?_is_native=true`, or have the developer confirm in the dashboard. Only implement enabled strategies; if the user asked for a disabled one (common with SMS), tell them to enable it in Clerk Dashboard → **User & authentication** first.
2. **Combined flow by default** — one screen that signs in or signs up, unless separation is requested.
3. **Captcha mount point** — every screen that can create a sign-up must render `<View nativeID="clerk-captcha" />`.
## Shared finalize helper
All flows end the same way. Session tasks (e.g. forced MFA enrollment, org selection) must be handled before navigating:
```tsx
const navigateAfterAuth = ({ session, decorateUrl }) => {
if (session?.currentTask) {
// Route to your session-task UI instead of home.
// https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
return
}
const url = decorateUrl('/')
if (url.startsWith('http')) window.location.href = url // Expo web
else router.push(url as Href)
}
// on status === 'complete':
await signIn.finalize({ navigate: navigateAfterAuth }) // same shape for signUp.finalize
```
## Email + password (sign-in)
```tsx
import { useSignIn } from '@clerk/expo'
const { signIn, errors, fetchStatus } = useSignIn()
const handleSubmit = async () => {
const { error } = await signIn.password({ emailAddress, password })
if (error) return // surface errors.fields.identifier / errors.fields.password in the UI
if (signIn.status === 'complete') {
await signIn.finalize({ navigate: navigateAfterAuth })
} else if (signIn.status === 'needs_second_factor') {
// MFA step — see below
} else if (signIn.status === 'needs_client_trust') {
// New-device verification: send a code, then verify with signIn.mfa.verifyEmailCode
const emailFactor = signIn.supportedSecondFactors?.find((f) => f.strategy === 'email_code')
if (emailFactor) await signIn.mfa.sendEmailCode()
}
}
```
Sign-up mirror: `signUp.password({ emailAddress, password })`, then `signUp.verifications.sendEmailCode()`, collect the code, `signUp.verifications.verifyEmailCode({ code })`, then `signUp.finalize(...)`. Show the verify step when `signUp.status === 'missing_requirements' && signUp.unverifiedFields.includes('email_address')`.
## Phone / SMS OTP
Requires **Phone number** + **SMS verification code** enabled in the dashboard (SMS is billable and instance-dependent — check first, Gate 3).
Sign-up:
```tsx
const { signUp, errors, fetchStatus } = useSignUp()
const handleSubmit = async () => {
const { error } = await signUp.create({ phoneNumber }) // E.164, e.g. +15551234567
if (!error) await signUp.verifications.sendPhoneCode()
}
const handleVerify = async () => {
await signUp.verifications.verifyPhoneCode({ code })
if (signUp.status === 'complete') await signUp.finalize({ navigate: navigateAfterAuth })
}
// Show the code input when:
// signUp.status === 'missing_requirements' && signUp.unverifiedFields.includes('phone_number') && signUp.missingFields.length === 0
// Resend: signUp.verifications.sendPhoneCode()
```
Sign-in:
```tsx
const { signIn, errors, fetchStatus } = useSignIn()
const handleSubmit = async () => {
// Creates the sign-in attempt AND sends the SMS in one call — no signIn.create() needed
const { error } = await signIn.phoneCode.sendCode({ phoneNumber })
if (error) return // surface errors.fields
}
const handleVerify = async () => {
await signIn.phoneCode.verifyCode({ code })
if (signIn.status === 'complete') await signIn.finalize({ navigate: navigateAfterAuth })
}
// Resend: signIn.phoneCode.sendCode() with no args — the sign-in already exists
```
Email OTP is the same shape: `emailCode.sendCode({ emailAddress })` (also self-creating) / `emailCode.verifyCode({ code })` on sign-in, `sendEmailCode()` / `verifyEmailCode()` on sign-up verifications.
## Combined sign-in-or-up
Attempt sign-in; on `form_identifier_not_found`, switch to sign-up with the same credentials:
```tsx
const { error } = await signIn.password({ emailAddress, password })
if (error) {
if (error.errors[0].code === 'form_identifier_not_found') {
const { error: signUpError } = await signUp.password({ emailAddress, password })
if (signUpError) return
await signUp.verifications.sendEmailCode()
if (signUp.unverifiedFields?.includes('email_address')) setShowCodeStep(true)
return
}
return // real error — surface errors.fields
}
// continue sign-in path (complete / needs_second_factor / needs_client_trust)
```
For the OTP-only variant, do the same with `signIn.phoneCode` / `signUp.create({ phoneNumber })`.
## MFA / 2FA (second factor)
When `signIn.status === 'needs_second_factor'`, check `signIn.supportedSecondFactors` and use `signIn.mfa`:
| Factor | Send | Verify |
|--------|------|--------|
| SMS code | `signIn.mfa.sendPhoneCode()` | `signIn.mfa.verifyPhoneCode({ code })` |
| Email code | `signIn.mfa.sendEmailCode()` | `signIn.mfa.verifyEmailCode({ code })` |
| TOTP (authenticator app) | — | `signIn.mfa.verifyTOTP({ code })` |
| Backup code | — | `signIn.mfa.verifyBackupCode({ code })` |
After a successful verify, `signIn.status` becomes `complete` → `finalize()`. The same `mfa` methods serve `needs_client_trust` (new-device verification).
## Other flows
- **Forgot password**: `signIn.resetPasswordEmailCode.sendCode()` → `verifyCode({ code })` (status becomes `needs_new_password`) → `submitPassword({ password })`. Phone variant: `signIn.resetPasswordPhoneCode.*`.
- **Email link**: `signIn.emailLink.sendLink({ ... })` + `signIn.emailLink.waitForVerification()`.
- **Passkeys**: `signIn.passkey()`; requires `@clerk/expo-passkeys` and dev build.
- **Social/SSO**: see sso-and-native-auth.md — SSO does not use this method API.
## Verification checklist
- No legacy API in generated code (no `prepareFirstFactor`, no `setActive({ session })` outside SSO).
- Every implemented strategy confirmed enabled for the instance.
- Errors surfaced from `errors.fields.*`; buttons disabled while `fetchStatus === 'fetching'`.
- `finalize({ navigate })` handles `session.currentTask` before navigating.
- Sign-up screens include `<View nativeID="clerk-captcha" />`.
- Verify step gated on `status` / `unverifiedFields`, with a resend button and a `reset()` escape hatch.
- One real end-to-end auth exercised; session survives restart.
references/prebuilt-components.md
# Prebuilt native components (@clerk/expo/native)
The default "add auth" path. `AuthView`, `UserButton`, and `UserProfileView` render Clerk's native UI — SwiftUI on iOS, Jetpack Compose on Android — and handle every enabled auth strategy (password, OTP, social, MFA) with no flow code.
> Canonical docs (fetch to re-verify if the installed SDK is newer than 3.6.x):
> https://clerk.com/docs/reference/expo/native-components/overview — plus `auth-view`, `user-button`, `user-profile-view`, `theming` pages alongside it
**Status and requirements — tell the developer up front:**
- Beta. Works well, but flag it before a production rollout decision.
- Requires a development build (`npx expo run:ios` / `run:android`). Not Expo Go, not web.
- Requires the `@clerk/expo` config plugin and a prebuild (see setup.md).
## Components
Import from `@clerk/expo/native`:
| Component | Renders | Props |
|-----------|---------|-------|
| `AuthView` | Sign-in/sign-up UI, inline (fills parent) | `mode?: 'signInOrUp' \| 'signIn' \| 'signUp'` (default `signInOrUp`), `isDismissible?: boolean` (default true), `onDismiss?: () => void` |
| `UserButton` | Avatar button that opens the native user profile | — |
| `UserProfileView` | Profile/account management, inline | `isDismissible?`, `onDismiss?`, `style?` |
These are the only public props. Do not invent event handlers (`onAuthEvent`, `onSignIn`, etc.) — react to auth state with `useAuth()` / `useUser()` instead. Verify props against `node_modules/@clerk/expo/dist/native/*.d.ts` for the installed version.
## Canonical screen
The components render inline; the app owns presentation. The docs pattern presents `AuthView` in a React Native `Modal`:
```tsx
// src/app/index.tsx
import { useAuth } from '@clerk/expo'
import { AuthView, UserButton } from '@clerk/expo/native'
import { useState } from 'react'
import { View, ActivityIndicator, Button, Modal } from 'react-native'
export default function MainScreen() {
const { isSignedIn, isLoaded } = useAuth({ treatPendingAsSignedOut: false })
const [isAuthOpen, setIsAuthOpen] = useState(false)
if (!isLoaded) return <ActivityIndicator size="large" />
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{isSignedIn ? <UserButton /> : <Button title="Sign in" onPress={() => setIsAuthOpen(true)} />}
<Modal
animationType="slide"
visible={isAuthOpen}
presentationStyle="pageSheet"
onRequestClose={() => setIsAuthOpen(false)}
>
<AuthView onDismiss={() => setIsAuthOpen(false)} />
</Modal>
</View>
)
}
```
Rules baked into this pattern:
1. **`useAuth({ treatPendingAsSignedOut: false })`** — required with native components so pending session tasks aren't treated as signed out mid-flow.
2. **Keep the `Modal` mounted at the same level as signed-in and signed-out content.** Rendering it only inside signed-out content unmounts it too early when auth state flips before session tasks finish.
3. **Session sync is automatic.** When `AuthView` completes, the JS SDK's `useAuth()`/`useUser()` update on their own. Never call `setActive()` and never add manual session plumbing.
4. **Keep `mode` at its default** (`signInOrUp`) unless the developer asks for separate flows.
5. **Don't add `useSignInWithGoogle()` / `useSignInWithApple()` buttons next to `AuthView`** — it renders every enabled social provider itself. Provider availability comes from the instance config; fix gaps in the Clerk Dashboard, not in code.
## Theming
The config plugin accepts a JSON theme applied to both platforms at prebuild:
```json
// app.json
{
"expo": {
"plugins": [["@clerk/expo", { "theme": "./clerk-theme.json" }]]
}
}
```
```json
// clerk-theme.json — every key optional; unknown keys warn
{
"colors": { "primary": "#6C47FF", "background": "#FFFFFF" },
"darkColors": { "primary": "#8B6FFF", "background": "#0B0B0F" },
"design": { "borderRadius": 12, "fontFamily": "Inter" }
}
```
Colors are 6- or 8-digit hex (validated at prebuild — invalid values fail the build with a descriptive error). Available color keys: `primary`, `background`, `input`, `danger`, `success`, `warning`, `foreground`, `mutedForeground`, `primaryForeground`, `inputForeground`, `neutral`, `border`, `ring`, `muted`, `shadow`. Rerun `npx expo prebuild --clean` (or `expo run:*`) after theme changes.
For customization beyond the theme schema, the answer is custom flows, not fighting the native UI.
## Verification checklist
- Dev build runs on device/simulator (not Expo Go); developer told about beta status.
- Provider + token cache per setup.md; config plugin registered; prebuild done.
- `treatPendingAsSignedOut: false` passed to the `useAuth()` call gating the auth UI.
- Auth modal mounted outside the signed-in/signed-out branch.
- No `setActive()`, no `onAuthEvent`, no native Google/Apple hooks alongside `AuthView`.
- One real sign-in completed; `useUser()` reflects the user; session survives app restart.
references/protected-routes.md
# Protected routes (Expo Router)
Gate screens on auth state with layout-level guards. Applies to both prebuilt and custom flows.
> Canonical doc: https://clerk.com/docs/expo/guides/users/reading (protect content and read user data)
## Recommended structure
```
src/app/
├── _layout.tsx # Root layout with ClerkProvider (see setup.md)
├── (auth)/ # Public: sign-in / sign-up
│ ├── _layout.tsx # Redirects signed-in users away
│ ├── sign-in.tsx
│ └── sign-up.tsx
└── (home)/ # Protected: app content
├── _layout.tsx # Redirects signed-out users to sign-in
└── index.tsx
```
Apps using `AuthView` in a modal (prebuilt path) often don't need an `(auth)` group at all — the modal lives beside the protected content (see prebuilt-components.md).
## Layout guards
```tsx
// src/app/(home)/_layout.tsx — protect the group
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'
export default function Layout() {
const { isSignedIn, isLoaded } = useAuth()
if (!isLoaded) return null
if (!isSignedIn) return <Redirect href="/(auth)/sign-in" />
return <Stack />
}
```
```tsx
// src/app/(auth)/_layout.tsx — keep signed-in users out of auth screens
export default function AuthRoutesLayout() {
const { isSignedIn, isLoaded } = useAuth()
if (!isLoaded) return null
if (isSignedIn) return <Redirect href="/" />
return <Stack />
}
```
Rules:
- **Always check `isLoaded` before `isSignedIn`** — Clerk needs a moment to restore the session from the token cache; skipping this flashes the sign-in screen at every cold start.
- Return `null` (or a splash/spinner) while loading.
- Use `<Redirect>` from expo-router, not `router.push` inside effects — avoids render-phase navigation warnings.
- Single screens can use the same `isLoaded`/`isSignedIn` + `<Redirect>` pattern inline.
## Conditional rendering without navigation
For showing/hiding content in place, use the `<Show>` control component:
```tsx
import { Show } from '@clerk/expo'
<Show when="signed-in">
<Text>Hello {user?.firstName}</Text>
</Show>
<Show when="signed-out">
<Link href="/(auth)/sign-in"><Text>Sign in</Text></Link>
</Show>
```
`ClerkLoaded` / `ClerkLoading` are also exported for load-state gating.
## Pitfall
Guards are client-side UX, not security. Anything sensitive must be enforced server-side — verify the Clerk session token on your backend (see recipes.md → "Calling your backend").
references/recipes.md
# Recipes: users, orgs, sign-out, backend calls, device features
Post-auth patterns. Hooks below are imported from `@clerk/expo` unless noted; they mirror `@clerk/react`.
> Canonical docs (fetch to re-verify if the installed SDK is newer than 3.6.x):
> https://clerk.com/docs/reference/expo/overview — plus `local-credentials` and `passkeys` pages under the Expo reference
## User profile data
```tsx
import { useUser } from '@clerk/expo'
const { user, isLoaded } = useUser()
// user is null until loaded/signed in — always guard
// user.fullName, user.firstName, user.imageUrl, user.primaryEmailAddress?.emailAddress
```
For a full profile management UI on a dev build, prefer the native `UserProfileView` / `UserButton` (prebuilt-components.md) over hand-built screens.
## Sign out
```tsx
import { useClerk } from '@clerk/expo'
const { signOut } = useClerk()
<Pressable onPress={() => signOut()}>...</Pressable>
```
## Organization switching (B2B)
Organizations must be enabled in the dashboard. For deeper org work (roles, invitations, RBAC) load the `clerk-orgs` skill — the hooks are identical in Expo.
```tsx
import { useOrganization, useOrganizationList } from '@clerk/expo'
const { organization } = useOrganization()
const { setActive, userMemberships } = useOrganizationList({
userMemberships: { infinite: true },
})
// Current: organization?.name ?? 'Personal account'
// Switch: setActive({ organization: membership.organization.id })
// List: userMemberships.data?.map((m) => m.organization) — guard while undefined
```
## Calling your backend
Route guards are client-side only; authorize on the server:
```tsx
import { useAuth } from '@clerk/expo'
const { getToken } = useAuth()
const res = await fetch(`${API_URL}/endpoint`, {
headers: { Authorization: `Bearer ${await getToken()}` },
})
```
Verify the token server-side with Clerk's backend SDK for your server framework (e.g. `@clerk/backend`'s `verifyToken`, or the framework SDK's `getAuth`). Clerk has no official Expo Router API-routes (`+api.ts`) integration — treat any server code as a normal backend and use `@clerk/backend`.
## Push notifications with user context
Associate the Expo push token with the Clerk user:
```tsx
import { useUser } from '@clerk/expo'
import * as Notifications from 'expo-notifications'
import { useEffect } from 'react'
export function PushTokenRegistrar() {
const { user, isLoaded } = useUser()
useEffect(() => {
if (!isLoaded || !user) return
;(async () => {
const { status } = await Notifications.requestPermissionsAsync()
if (status !== 'granted') return
const token = (await Notifications.getExpoPushTokenAsync()).data
await user.update({
unsafeMetadata: { ...user.unsafeMetadata, expoPushToken: token },
})
})()
}, [isLoaded, user])
return null
}
```
- `unsafeMetadata` is client-writable; anything that must be trusted belongs in `publicMetadata`, written server-side via the Backend SDK.
- Server send: look up the user with the Backend SDK, read `unsafeMetadata.expoPushToken`, POST to `https://exp.host/--/api/v2/push/send`.
- Re-register after sign-out/sign-in as a different user.
## Biometric re-auth — `useLocalCredentials()`
Dev build only. Stores the user's credentials in the keychain, gated by Face ID / Touch ID / device biometrics, for fast re-sign-in.
Prerequisites: `npx expo install expo-local-authentication` (plus `expo-secure-store` from setup), iOS `NSFaceIDUsageDescription` in `app.json` → `ios.infoPlist`.
```tsx
import { useLocalCredentials } from '@clerk/expo/local-credentials'
const { hasCredentials, setCredentials, authenticate, biometricType } = useLocalCredentials()
// After a successful password sign-in, offer to enable biometrics:
await setCredentials({ identifier: emailAddress, password })
// On later launches, if hasCredentials:
const signInResource = await authenticate() // prompts biometrics, performs the sign-in
```
Password-based instances only — it replays stored credentials. Confirm the exact return shape against `node_modules/@clerk/expo/dist/local-credentials/*.d.ts` before wiring UI.
## Passkeys
Dev build only. Install the separate `@clerk/expo-passkeys` package, then import from the `@clerk/expo/passkeys` subpath — the subpath re-exports the peer package (this install/import pairing is intentional and matches the docs). Pass it to the provider and enable passkeys in the dashboard:
```tsx
import { passkeys } from '@clerk/expo/passkeys'
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache} __experimental_passkeys={passkeys}>
```
Requires associated-domains setup (iOS) / asset links (Android). Fetch https://clerk.com/docs/reference/expo/passkeys for the current platform configuration before implementing — this surface is still experimental and changes.
## Offline resource caching
If Clerk resources (user, session) should survive offline cold starts, pass `resourceCache` from `@clerk/expo/resource-cache` to the provider (`__experimental_resourceCache`). `@clerk/expo/secure-store` is the deprecated alias — never use it in new code.
references/setup.md
# Setup: install, provider, token cache, builds
Shared prerequisites for every path (prebuilt or custom). Complete this file top to bottom before implementing a flow.
> Canonical doc (fetch to re-verify if the installed SDK is newer than 3.6.x): https://clerk.com/docs/getting-started/quickstart (Expo SDK tab)
## Supported versions
Written against `@clerk/expo` v3.6.x (July 2026). Peer requirements: Expo SDK 53–56, React Native ≥ 0.75, React 18 or 19. If the project's Expo SDK or RN version is below the floor, upgrading the app comes before adding Clerk.
## 1. Install
```bash
npx expo install @clerk/expo expo-secure-store
# add expo-dev-client if the app will use native components or native sign-in hooks
npx expo install expo-dev-client
```
`npx expo install` (not `npm install`) ensures Expo-SDK-compatible versions. Additional peer deps are per-strategy — install only what the selected flow needs:
| Feature | Install |
|---------|---------|
| Browser SSO/OAuth (`useSSO`) | `npx expo install expo-auth-session expo-web-browser` |
| Native Google sign-in | `npx expo install expo-crypto` |
| Native Apple sign-in | `npx expo install expo-apple-authentication` |
| Biometrics (`useLocalCredentials`) | `npx expo install expo-local-authentication` |
| Passkeys | `npx expo install @clerk/expo-passkeys` |
Alternative bootstrap: `npx clerk@latest init --framework expo` installs `@clerk/expo` and writes the publishable key to the env file (it does not scaffold screens).
## 2. Publishable key
`.env` at the project root:
```env
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
```
Rules:
- The prefix must be `EXPO_PUBLIC_` — Metro only inlines env vars with this prefix into the client bundle.
- Read the variable in app code and pass it explicitly to `ClerkProvider`. Env reads inside `node_modules` are not inlined in production builds.
- If no key is available, ask the developer (Clerk Dashboard → API keys) and wait.
## 3. Dashboard prerequisites
- **Native API enabled**: Clerk Dashboard → **Native applications** (`https://dashboard.clerk.com/~/native-applications`). Required for native apps to talk to Clerk.
- The auth strategies the app will use (email, phone/SMS, social providers) are enabled under **User & authentication**.
## 4. Config plugin
Verify `app.json` / `app.config.js` includes both plugins (`npx expo install` usually adds them):
```json
{
"expo": {
"plugins": ["expo-secure-store", "@clerk/expo"]
}
}
```
The `@clerk/expo` plugin registers the native modules (iOS min deployment target 17.0), wires Google sign-in when configured, and optionally accepts a `theme` option for native components (see prebuilt-components.md). Plugin changes require a fresh prebuild: `npx expo prebuild --clean` or rerunning `expo run:*`.
Apps using browser SSO also need a deep-link scheme in `app.json`: `"scheme": "yourapp"`.
## 5. ClerkProvider + token cache
Root layout (Expo Router):
```tsx
// src/app/_layout.tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Slot } from 'expo-router'
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
if (!publishableKey) {
throw new Error('Add your Clerk Publishable Key to the .env file')
}
export default function RootLayout() {
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
<Slot />
</ClerkProvider>
)
}
```
- `tokenCache` persists the session in the device keychain via `expo-secure-store` (`AFTER_FIRST_UNLOCK`), so sessions survive app restarts. Without it, tokens live in memory only.
- On web, `tokenCache` is `undefined` and Clerk falls back to its web storage — no platform branching needed.
- Only implement a custom `TokenCache` if the developer has a stated requirement (e.g. different keychain accessibility). Never AsyncStorage — it is unencrypted.
- `ClerkProvider` calls `WebBrowser.maybeCompleteAuthSession()` internally; never add it manually.
- Do not add provider props that match defaults.
## 6. Expo Go vs development build vs web
| Capability | Expo Go | Dev build | Web |
|------------|---------|-----------|-----|
| Custom flows (`useSignIn`, `useSignUp`, hooks) | Yes | Yes | Yes |
| Browser SSO (`useSSO`) | Yes | Yes | Yes |
| Native components (`@clerk/expo/native`) | No | Yes | No |
| Native Google/Apple hooks | No | Yes | No |
| Biometrics, passkeys | No | Yes | No |
| Web UI components (`@clerk/expo/web`) | — | — | Yes |
Dev build commands: `npx expo run:ios` / `npx expo run:android` (or EAS Build). State this requirement before implementing anything in the "No under Expo Go" rows.
For Expo web targets, `@clerk/expo/web` exports the standard Clerk web components (`SignIn`, `SignUp`, `UserButton`, `UserProfile`, `OrganizationSwitcher`, `PricingTable`, …); they throw if rendered on native, so gate on `Platform.OS === 'web'`.
## 7. Verify setup before building flows
- App boots with the provider mounted and no key errors.
- `useAuth()` returns `isLoaded: true` shortly after launch.
- After the first real sign-in later: kill and relaunch the app — the session must persist.
references/sso-and-native-auth.md
# Social auth: browser SSO and native Google/Apple
> Canonical docs (fetch to re-verify if the installed SDK is newer than 3.6.x):
> - OAuth custom flow: https://clerk.com/docs/guides/development/custom-flows/authentication/oauth-connections
> - `useSSO`: https://clerk.com/docs/reference/expo/native-hooks/use-sso
> - Native Google/Apple: https://clerk.com/docs/reference/expo/native-hooks/use-sign-in-with-google and `use-sign-in-with-apple`
Three ways to do social auth in Expo. Pick by platform and build type:
| Approach | UX | Works in Expo Go | Platforms |
|----------|-----|------------------|-----------|
| `AuthView` (prebuilt) | Native, renders all enabled providers | No (dev build) | iOS, Android |
| `useSSO()` browser flow | Opens a browser session | Yes | iOS, Android, web |
| `useSignInWithGoogle()` / `useSignInWithApple()` | Fully native sheet, no browser | No (dev build) | Google: iOS+Android; Apple: iOS only |
If the app already uses `AuthView`, stop — it handles social providers itself; configure providers in the dashboard instead. Whatever the approach, the provider must be enabled in Clerk Dashboard → **User & authentication → Social connections** (Gate 3).
## Browser SSO — `useSSO()`
Never `useOAuth()` (deprecated). Prerequisites: `npx expo install expo-auth-session expo-web-browser`, and a deep-link `"scheme"` in `app.json`.
```tsx
import { useSSO } from '@clerk/expo'
const { startSSOFlow } = useSSO()
const onPress = async () => {
try {
const { createdSessionId, setActive, signUp } = await startSSOFlow({
strategy: 'oauth_google', // oauth_apple, oauth_github, oauth_microsoft, ...
})
if (createdSessionId) {
await setActive!({ session: createdSessionId })
router.replace('/')
} else if (signUp?.status === 'missing_requirements') {
// Instance requires fields the provider didn't supply (e.g. username) —
// collect them and signUp.update(...), or relax requirements in the dashboard
}
// No createdSessionId and no missing requirements → user cancelled; do nothing.
} catch (err) {
console.error(JSON.stringify(err, null, 2))
}
}
```
Behavior that is handled for you — do not reimplement:
- `redirectUrl` defaults to `AuthSession.makeRedirectUri({ path: 'sso-callback' })`; only override for a custom callback route.
- `ClerkProvider` calls `WebBrowser.maybeCompleteAuthSession()` — never add it manually.
- The transfer flow (account exists → sign-in, new account → sign-up with `transfer: true`) runs inside `startSSOFlow`.
- User cancellation is not an error: it resolves with `createdSessionId: null`. Don't show error UI for it.
Enterprise SSO/SAML: `startSSOFlow({ strategy: 'enterprise_sso', identifier: email })`.
Note: SSO is the one flow that still uses `setActive({ session: createdSessionId })` — the `finalize()` method from custom-flows.md does not apply here.
## Native Google sign-in — `useSignInWithGoogle()`
Fully native Google sheet (Credential Manager on Android). Dev build only.
Setup:
1. `npx expo install expo-crypto`
2. Env vars in `.env` (values from the Google Cloud OAuth clients configured for the Clerk instance):
- `EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID` (always required)
- `EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID` (iOS)
- `EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME` (iOS — the config plugin writes it into the iOS URL types at prebuild; prebuild fails without it)
3. `@clerk/expo` config plugin registered, then rebuild.
Full provider-side setup lives at https://clerk.com/docs/guides/configure/auth-strategies/sign-in-with-google — fetch it if the Google Cloud side isn't already configured.
```tsx
import { useSignInWithGoogle } from '@clerk/expo/google'
import { Platform } from 'react-native'
const { startGoogleAuthenticationFlow } = useSignInWithGoogle()
const onPress = async () => {
try {
const { createdSessionId, setActive } = await startGoogleAuthenticationFlow()
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
router.replace('/')
}
} catch (err: any) {
if (err.code === 'SIGN_IN_CANCELLED' || err.code === '-5') return // user cancelled
console.error('Sign in with Google error:', JSON.stringify(err, null, 2))
}
}
// Render the button only where supported:
if (Platform.OS !== 'ios' && Platform.OS !== 'android') return null
```
Always wrap in try/catch and swallow the cancellation codes. On unsupported platforms (web), fall back to `useSSO({ strategy: 'oauth_google' })` or hide the button.
**Next-major note**: native Google sign-in moves to a separate `@clerk/expo-google-signin` package (plus its own config plugin) in the next major version. On v3 the `@clerk/expo/google` import is correct and logs a dev-only migration warning — don't preinstall the new package.
## Native Apple sign-in — `useSignInWithApple()`
iOS only; dev build only. Requires `npx expo install expo-apple-authentication` and the Sign in with Apple capability (the `expo-apple-authentication` plugin handles the entitlement — add it to `app.json` plugins). Full setup: https://clerk.com/docs/guides/configure/auth-strategies/sign-in-with-apple
```tsx
import { useSignInWithApple } from '@clerk/expo/apple'
const { startAppleAuthenticationFlow } = useSignInWithApple()
// identical result handling to the Google hook: setActive on createdSessionId, swallow cancellation
```
On Android/web, fall back to `useSSO({ strategy: 'oauth_apple' })` or hide the button. App Store policy: apps offering third-party sign-in on iOS generally must also offer Sign in with Apple — mention this when adding Google-only auth to an iOS app.
## Verification checklist
- Provider enabled in the dashboard before writing code.
- `useSSO` path: scheme configured, no manual `maybeCompleteAuthSession`, cancellation silent, `setActive` called on `createdSessionId`.
- Native hooks: dev build stated, platform-gated rendering, cancellation codes swallowed, browser-SSO or hidden-button fallback for unsupported platforms.
- Not paired with `AuthView`.
- One real provider round-trip tested on a device or simulator.
SKILL.md
---
name: clerk-expo
description: Add Clerk authentication to Expo and React Native apps using @clerk/expo.
Use for Expo setup, prebuilt native components (AuthView, UserButton), custom sign-in/sign-up
flows (email, password, SMS/phone OTP, MFA), OAuth/SSO, native Google/Apple sign-in,
Expo Router protected routes, biometrics, and push notifications. Do not use for
native Swift/iOS, native Android/Kotlin, or web-only framework projects.
license: MIT
allowed-tools: WebFetch
metadata:
author: clerk
version: 2.0.0
compatibility: Requires @clerk/expo v3.4+ (written against v3.6.x, July 2026). Expo SDK 53-56, React Native 0.75+.
---
# Clerk Expo (React Native)
Implement Clerk in Expo / React Native projects. This skill inlines verified patterns for the stable surface (provider, token cache, flows) and requires source inspection of the installed `@clerk/expo` package for anything volatile (component props, hook signatures).
## Activation Rules
Activate when either is true:
- The user asks for auth in an Expo or React Native app, or mentions `@clerk/expo`, `ClerkProvider`, Expo Router auth, or Clerk hooks in a native app.
- The project is Expo/React Native (`app.json` / `app.config.js`, `expo` in `package.json`, `metro.config.js`, `@clerk/expo` dependency).
Route away when:
- Native iOS/Swift project (`.xcodeproj`, `Package.swift`) → `clerk-swift`
- Native Android/Kotlin project (`build.gradle` without React Native) → `clerk-android`
- Web-only framework (Next.js, Remix, plain React, etc.) → the matching framework skill
## Intent Map
Match what the user asked for, then load the reference(s) listed. Load only what the task needs.
| User intent (examples) | Path | Reference |
|------------------------|------|-----------|
| "Add auth to my app" / "add sign-in with Clerk" | Prebuilt native components (default) | references/setup.md + references/prebuilt-components.md |
| "Add auth" but Expo Go / web / custom UI required | Custom flows | references/setup.md + references/custom-flows.md |
| "Add phone / SMS auth", "email OTP", "passwordless" | Custom flow, `phoneCode` / `emailCode` | references/custom-flows.md |
| "Sign in with Google/Apple/GitHub", "social login", "SSO" | Browser SSO or native buttons | references/sso-and-native-auth.md |
| "MFA / 2FA / TOTP", "forgot password", "email link" | Custom flow additions | references/custom-flows.md |
| "Protect routes/screens", "redirect if signed out" | Expo Router guards | references/protected-routes.md |
| "Show user profile", "org switching", "push notifications", "sign out", "call my backend" | App recipes | references/recipes.md |
| "Biometric login", "Face ID", "passkeys" | Device features | references/recipes.md |
## Default Path Decision
When the user says "add auth" without specifying UI:
1. **Default to prebuilt native components** (`AuthView` + `UserButton` from `@clerk/expo/native`). Fastest to working auth; UI is maintained by Clerk. Tell the developer they are in beta and require a development build.
2. **Fall back to custom flows** when any of these hold — say why when you switch:
- The project must run in Expo Go (no dev build).
- The app targets web (native components don't render on web).
- The developer wants their own UI or a specific brand experience beyond theming.
3. If the developer has an existing auth UI, extend what's there — don't rip out custom flows to insert `AuthView` (or vice versa) without being asked.
Do not blend prebuilt components and custom flows for the same auth step (e.g. `AuthView` plus a custom password form). Blending is allowed only when the developer explicitly asks.
## Quick Workflow
1. Confirm project type (Expo/RN) and pick the path per the Intent Map / Default Path rules.
2. Follow references/setup.md: install, env key, provider, token cache, config plugin, build type.
3. Verify dashboard prerequisites (Gate 2 and Gate 3 below).
4. Implement from the selected reference only.
5. Verify by building, not just by writing:
- Run the project's typecheck (`npx tsc --noEmit` or equivalent).
- Build and launch: `npx expo run:ios` / `run:android` for native features, `npx expo start` for Expo Go flows. If the build fails, fix and rebuild iteratively — build errors against the installed SDK are the ground truth when this skill and the SDK disagree. After ~5 failed fix attempts, stop and ask the developer how to proceed instead of thrashing.
- Walk the developer through one real sign-in, then confirm the session survives an app restart (token cache working).
## Execution Gates (Do Not Skip)
1. **Publishable key** — Read from `process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` (`.env` file). Never `NEXT_PUBLIC_`, never hardcoded. If no key exists, ask the developer for one (or run `npx clerk@latest init --framework expo`, which installs the SDK and writes the env file) and wait before editing files.
2. **Native API dashboard toggle** — Clerk's Native API must be enabled for the instance: Clerk Dashboard → **Native applications** (`https://dashboard.clerk.com/~/native-applications`). Tell the developer to verify this during setup; it is required for any native integration.
3. **Factor availability** — Before implementing a specific strategy (SMS, email code, social provider), confirm it's enabled for the instance. Derive the Frontend API URL from the publishable key (base64-decode the middle segment) and fetch `<frontendApiUrl>/v1/environment?_is_native=true`, or ask the developer to check the dashboard (**User & authentication**). SMS in particular is instance-configuration-dependent — code written for a disabled factor fails at runtime, not build time.
4. **Current custom-flows API only** — `useSignIn()` / `useSignUp()` from `@clerk/expo` (v3.4+) return `{ signIn, errors, fetchStatus }` and use method-based flows: `signIn.password()`, `signIn.phoneCode.sendCode()`, `signIn.finalize()`. Never generate the legacy pattern: destructuring `isLoaded`/`setActive` from `useSignIn()`/`useSignUp()` (the current hooks don't return them), or `signIn.create()` chained with `prepareFirstFactor()`/`attemptFirstFactor()` + `setActive({ session })`. That pattern lives at `@clerk/expo/legacy` and is only for maintaining existing legacy code, never for new work. Scope notes: `isLoaded` from `useAuth()`/`useUser()` is current API and required in guards; `signIn.create()` itself still exists for advanced cases — prefer the factor-specific methods.
5. **`useSSO()`, never `useOAuth()`** — `useOAuth` is deprecated. Note the asymmetry: `startSSOFlow()` still returns `{ createdSessionId, setActive }` and requires `setActive({ session: createdSessionId })` — SSO does not use `finalize()`.
6. **Token cache** — `tokenCache` from `@clerk/expo/token-cache` on `ClerkProvider`. Never use `expo-secure-store` directly for session tokens, never AsyncStorage.
7. **`resourceCache`, never `secureStore`** — if offline resource caching comes up, `@clerk/expo/secure-store` is deprecated; use `resourceCache` from `@clerk/expo/resource-cache`.
8. **Build-type gating** — Native components (`@clerk/expo/native`) and native hooks (`useSignInWithGoogle`, `useSignInWithApple`, `useLocalCredentials`) require a development build (`npx expo run:ios` / `run:android`), not Expo Go, and don't exist on web. For web targets use `@clerk/expo/web` components or custom flows. State the build requirement before implementing a native-only feature.
9. **Combined sign-in-or-up default** — one combined flow unless the developer asks for separate sign-in and sign-up screens.
10. **Bot protection** — custom sign-up screens must render `<View nativeID="clerk-captcha" />`; Clerk's bot protection is on by default and needs this mount point.
11. **Source verification for volatile surfaces** — before using native component props or native hook options, confirm against the installed package: `node_modules/@clerk/expo/dist/native/*.d.ts` and `package.json` `exports`. The installed version wins over this skill if they disagree.
12. **Freshness gate** — this skill was verified against `@clerk/expo` 3.6.x. Check the installed version (`node_modules/@clerk/expo/package.json`). If it is a newer minor or major, treat this skill's code snippets as suspect: re-verify against the docs URL cited next to each snippet (every reference section carries one) or the installed `.d.ts` before using them. If it is older than 3.4, the method-based custom-flows API may not exist — offer an upgrade instead of writing legacy code.
## Version Notes (v3.5–v3.6, June 2026)
- Minimum React Native raised to **0.75** in v3.5.0 (iOS SDK now links via SPM podspec). Peer range: `expo >=53 <57`.
- Native components matured: iOS moved to Expo Modules; native↔JS session sync is automatic and bidirectional — never call `setActive()` after native-component auth.
- The config plugin accepts a `theme` JSON file for native component styling (see references/prebuilt-components.md).
- Native Google sign-in will move to a separate `@clerk/expo-google-signin` package in the next major (the `@clerk/expo/google` import keeps working in v3; a dev warning announces the migration). Don't preinstall the new package on v3.
## Common Pitfalls
| Level | Issue | Prevention |
|-------|-------|------------|
| CRITICAL | Generating legacy custom-flow code (`signIn.create` + `prepareFirstFactor` + `setActive`) | Use the current method-based API (Gate 4) |
| CRITICAL | Using `useOAuth()` | Use `useSSO()` (Gate 5) |
| CRITICAL | Implementing SMS/social auth without checking the factor is enabled | Check environment/dashboard first (Gate 3) |
| CRITICAL | Native components targeted at Expo Go or web | Require a dev build; offer custom flows otherwise (Gate 8) |
| CRITICAL | Sign-up screen missing `<View nativeID="clerk-captcha" />` | Always include it (Gate 10) |
| HIGH | `NEXT_PUBLIC_` env prefix, or env var read inside `node_modules` | `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY`, passed explicitly to `ClerkProvider` |
| HIGH | Session lost on restart | `tokenCache` from `@clerk/expo/token-cache` on the provider |
| HIGH | Calling `setActive()` after `AuthView` / `UserButton` auth | Native components sync sessions automatically |
| HIGH | Pairing `AuthView` with `useSignInWithGoogle`/`useSignInWithApple` | `AuthView` renders enabled social providers itself |
| HIGH | Calling `WebBrowser.maybeCompleteAuthSession()` manually | `ClerkProvider` handles it |
| HIGH | Splitting sign-in / sign-up without being asked | Combined flow by default (Gate 9) |
| MEDIUM | Missing `isLoaded` check before `isSignedIn` in guards | Always gate on `isLoaded` first |
| MEDIUM | Using `yalc`/`pnpm link` for local `@clerk/expo` development | Use Verdaccio or pkg.pr.new |
## See Also
- `clerk` — top-level router
- `clerk-swift` / `clerk-android` — native mobile SDKs
- `clerk-orgs`, `clerk-billing`, `clerk-webhooks` — feature skills (hooks work the same in Expo)
- Installed package source: `node_modules/@clerk/expo/`
- https://clerk.com/docs/getting-started/quickstart (Expo SDK tab)
- https://clerk.com/docs/reference/expo/overview
- https://github.com/clerk/clerk-expo-quickstart — three official example apps: JS-only (Expo Go), JS + native sign-in buttons, native components