
clerk-expo-patterns
★ 54by clerk · part of clerk/skills
Expo / React Native patterns with Clerk — SecureStore token cache, OAuth
This is the playbook your agent receives when the skill activates — you don't need to read it to use the skill, but it's here to audit before installing.
name: clerk-expo-patterns description: 'Expo / React Native patterns with Clerk — SecureStore token cache, OAuth deep linking, useAuth in native, Expo Router protected routes, push notifications with user context. Triggers on: expo clerk, clerk react native, SecureStore token cache, expo router auth, OAuth deep link clerk, mobile auth clerk.' license: MIT allowed-tools: WebFetch metadata: author: clerk version: 1.0.0 compatibility: Requires Expo CLI and expo-secure-store package
Expo Patterns
SDK: @clerk/expo v3+. Requires Expo 53+, React Native 0.73+.
What Do You Need?
| Task | Reference |
|---|---|
| Persist tokens with SecureStore | references/token-storage.md |
| OAuth (Google, Apple, GitHub) | references/oauth-deep-linking.md |
| Protected screens with Expo Router | references/protected-routes.md |
| Push notifications with user data | references/push-notifications.md |
Mental Model
Clerk stores the session token in memory by default. In native apps:
- SecureStore — encrypt token in device keychain (recommended for production)
tokenCache— prop on<ClerkProvider>that provides custom storageuseAuth— same API as web, works in any component- OAuth — requires
useSSO+ deep link scheme configured inapp.json
Built-in Token Cache
import { tokenCache } from '@clerk/expo/token-cache'This uses expo-secure-store with keychainAccessible: AFTER_FIRST_UNLOCK. Install the peer dep:
npx expo install expo-secure-storeAuth Hooks
import { useAuth, useUser, useSignIn, useSignUp, useClerk } from '@clerk/expo'
export function ProfileScreen() {
const { isSignedIn, userId, signOut } = useAuth()
const { user } = useUser()
if (!isSignedIn) return <Redirect href="/sign-in" />
return (
<View>
<Text>{user?.fullName}</Text>
<Button title="Sign Out" onPress={() => signOut()} />
</View>
)
}OAuth Flow (Google)
import { useSSO } from '@clerk/expo'
import * as WebBrowser from 'expo-web-browser'
WebBrowser.maybeCompleteAuthSession()
export function GoogleSignIn() {
const { startSSOFlow } = useSSO()
const handlePress = async () => {
try {
const { createdSessionId, setActive } = await startSSOFlow({
strategy: 'oauth_google',
redirectUrl: 'myapp://oauth-callback',
})
if (createdSessionId) await setActive!({ session: createdSessionId })
} catch (err) {
console.error(err)
}
}
return <Button title="Continue with Google" onPress={handlePress} />
}Org Switching
import { useOrganization, useOrganizationList } from '@clerk/expo'
export function OrgSwitcher() {
const { organization } = useOrganization()
const { setActive, userMemberships } = useOrganizationList()
return (
<View>
<Text>Current: {organization?.name ?? 'Personal'}</Text>
{userMemberships.data?.map(mem => (
<Button
key={mem.organization.id}
title={mem.organization.name}
onPress={() => setActive({ organization: mem.organization.id })}
/>
))}
</View>
)
}Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
publishableKey undefined in prod | Using env var without EXPO_PUBLIC_ | Rename to EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY |
| Token lost on app restart | No tokenCache | Pass tokenCache from @clerk/expo/token-cache |
| OAuth redirect not working | Missing scheme in app.json | Add "scheme": "myapp" to app.json |
WebBrowser.maybeCompleteAuthSession | Not called | Call it at the top level of the OAuth callback screen |
useSSO not found | Old @clerk/expo version | useSSO replaced useOAuth in v3+ |
Import Map
| What | Import From |
|---|---|
ClerkProvider | @clerk/expo |
tokenCache | @clerk/expo/token-cache |
useAuth, useUser, useSignIn | @clerk/expo |
useSSO | @clerk/expo |
useOrganization, useOrganizationList | @clerk/expo |
See Also
clerk-setup- Initial Clerk installclerk-custom-ui- Custom flows & appearanceclerk-orgs- B2B organizations
Docs
npx skills add https://github.com/clerk/skills --skill clerk-expo-patternsRun this in your project — your agent picks the skill up automatically.
Minimal Setup
app/_layout.tsx
import { ClerkProvider } from '@clerk/expo'
import { tokenCache } from '@clerk/expo/token-cache'
import { Stack } from 'expo-router'
const publishableKey = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!
export default function RootLayout() {
return (
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
<Stack />
</ClerkProvider>
)
}CRITICAL: Use
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY— notNEXT_PUBLIC_. Env vars insidenode_modulesare not inlined in production builds. Always passpublishableKeyexplicitly.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.