
Clerk vs. Firebase Authentication for Expo
Auth for mobile is harder than web. Expo strips away a lot of React Native's rough edges, but your auth provider choice still shapes the entire project: what works in Expo Go, what needs a dev build, how sessions persist, how upgrades go.
This comparison covers Clerk and Firebase Authentication specifically for Expo apps. Clerk is a dedicated authentication platform with an Expo-first SDK. Firebase Auth is part of Google's broader backend-as-a-service ecosystem. Different philosophies, different tradeoffs.
Why compare now? Expo SDK 53 shifted the playing field. Metro bundler's package.json exports support broke Firebase JS SDK integrations, triggering "Component auth has not been registered yet" errors and initializeAuth() crashes on Hermes (Expo SDK 53 Changelog, 2025; expo#36588; firebase-js-sdk#9020). Workarounds exist (disabling package exports in Metro config), but each subsequent Expo SDK release has introduced new Firebase friction. Clerk shipped Core 3 with native SwiftUI and Jetpack Compose components. The gap between these two has widened since early 2025, and the decision matters more than it used to.
Quick comparison
Understanding the two approaches
Clerk: auth is the entire product
Clerk is a dedicated auth platform. The Expo SDK (@clerk/expo) is a first-party package maintained by Clerk's team. It uses a hybrid stateful + stateless architecture: short-lived 60-second session tokens auto-refresh in the background, stored in encrypted on-device storage via expo-secure-store (How Clerk Works).
Because auth is the whole product, Clerk goes deeper on auth-specific features: passkeys, native SwiftUI/Jetpack Compose components, built-in organizations, and prebuilt UI that works out of the box. Expo's own authentication guide lists Clerk first, describing it as a "powerful, full-featured authentication service with excellent Expo support" (Expo Authentication Guide, 2026).
Firebase Auth: auth as part of a bigger ecosystem
Firebase Auth is one piece of Google's BaaS (Firestore, Cloud Functions, Storage, Analytics). For Expo, two SDK paths exist, which is itself a source of confusion:
Firebase JS SDK (firebase npm package): first-party Google, works in Expo Go, limited to web-compatible features. No Analytics or Crashlytics on mobile. Requires manual persistence setup via getReactNativePersistence + AsyncStorage. Expo SDK 53+ requires firebase@^12.0.0; earlier versions cause ES module resolution errors (Expo Firebase guide).
React Native Firebase (@react-native-firebase/* by Invertase): community-maintained, requires a dev build, wraps native iOS/Android SDKs, supports all Firebase services. Auth persistence is handled natively.
Expo SDK compatibility at a glance
The architectural tradeoff is clear. Clerk goes deeper on auth. Firebase gives you a database, functions, and storage alongside auth, but auth gets less focused attention.
How this comparison is structured
This article evaluates Clerk and Firebase across five dimensions. The table below shows what each measures and which provider the evidence favors, so you can weight them against your own priorities.
These dimensions aren't numerically scored. The evidence is presented for each throughout the article. Your team's priorities determine which dimensions matter most.
Developer experience and setup
Clerk setup
Two packages, one environment variable, and a provider wrapper. This works in Expo Go for basic flows (Expo Using Clerk Guide; Hyperknot Auth Provider Comparison, 2024).
Install:
npx expo install @clerk/expo expo-secure-storeAdd your publishable key to .env:
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...Wrap your root layout with ClerkProvider:
import { ClerkProvider, ClerkLoaded } 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}
>
<ClerkLoaded>
<Slot />
</ClerkLoaded>
</ClerkProvider>
)
}Check auth state and conditionally render:
import { useAuth } from '@clerk/expo'
import { Show } from '@clerk/expo/ui'
export default function Home() {
const { signOut } = useAuth()
return (
<Show when="signed-in" fallback={<SignInScreen />}>
<Text>You're signed in!</Text>
<Button title="Sign out" onPress={() => signOut()} />
</Show>
)
}Build a sign-in screen with the Core 3 hooks API:
import { useSignIn } from '@clerk/expo'
import { useState } from 'react'
import { View, TextInput, Button, Text } from 'react-native'
import { useRouter, type Href } from 'expo-router'
export function SignInScreen() {
const { signIn, errors, fetchStatus } = useSignIn()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const router = useRouter()
const onSignIn = async () => {
const { error } = await signIn.password({ emailAddress: email, password })
if (error) return
if (signIn.status === 'complete') {
await signIn.finalize({
navigate: ({ session, decorateUrl }) => {
if (session?.currentTask) return
const url = decorateUrl('/')
router.push(url as Href)
},
})
} else if (signIn.status === 'needs_second_factor') {
// Handle MFA: signIn.mfa.sendEmailCode(), etc.
router.push('/(auth)/mfa')
}
}
return (
<View>
<TextInput placeholder="Email" value={email} onChangeText={setEmail} />
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
{errors?.fields?.password && <Text>{errors.fields.password.message}</Text>}
<Button title="Sign in" onPress={onSignIn} disabled={fetchStatus === 'fetching'} />
</View>
)
}That's it. Four files, working auth. The Expo quickstart has the full walkthrough.
Firebase setup (abbreviated)
Firebase requires choosing an SDK path first, and the choice has cascading consequences.
Option A: Firebase JS SDK (works in Expo Go, limited features):
import { initializeApp } from 'firebase/app'
import { initializeAuth, getReactNativePersistence } from 'firebase/auth'
import AsyncStorage from '@react-native-async-storage/async-storage'
const app = initializeApp(firebaseConfig)
// Without this, users get logged out every time the app closes
const auth = initializeAuth(app, {
persistence: getReactNativePersistence(AsyncStorage),
})Option B: React Native Firebase (dev build required, full native features):
Requires a development build with native modules, config plugin setup in app.json, and google-services.json / GoogleService-Info.plist configuration. No Expo Go support.
Then you build your own auth state listener:
import auth from '@react-native-firebase/auth'
import { useEffect, useState } from 'react'
function useFirebaseAuth() {
const [user, setUser] = useState(null)
useEffect(() => {
return auth().onAuthStateChanged(setUser)
}, [])
return user
}After that, you build every auth screen from scratch: sign-in, sign-up, password reset, MFA enrollment, profile management. Firebase provides zero prebuilt UI components for React Native.
Time to first auth
Clerk: install, wrap with provider, use hooks. Works in Expo Go.
Firebase: choose SDK path, configure native files or persistence, build custom UI from scratch. The JS SDK works in Expo Go for basic email/password, but native features (Google Sign-In, Apple Sign-In) require a dev build and React Native Firebase.
Authentication methods
Passkeys
Clerk has native passkey support via @clerk/expo-passkeys since February 2025. Users create passkeys with user.createPasskey() and sign in with signIn.passkey(). Requires iOS 16+ or Android 9+ and a development build.
Firebase has no passkey support. The FIDO Alliance reports that over 15 billion online accounts can now sign in using passkeys as of late 2024 (FIDO Alliance Passkey Index, Oct 2025), with a 93% login success rate compared to 63% for passwords. This is a significant gap for any auth provider to have.
Native sign-in
Clerk's native sign-in hooks use platform APIs directly:
- Native Apple Sign-In:
useSignInWithApple()wraps Apple'sASAuthorizationframework. iOS only. Requires a dev build andexpo-apple-authentication+expo-crypto. Released November 2025 (Clerk Changelog, Nov 2025). - Native Google Sign-In:
useSignInWithGoogle()usesASAuthorizationon iOS and Credential Manager on Android. Requires a dev build andexpo-crypto. No additional Google sign-in packages needed (the@clerk/expoconfig plugin handles it). Released March 2026 (Clerk Changelog, Mar 2026).
Firebase's native sign-in requires React Native Firebase plus separate community packages: @react-native-google-signin/google-signin and @invertase/react-native-apple-authentication. Dev build required. The Firebase JS SDK only offers browser-based OAuth for Google and Apple (Expo Firebase Guide; Expo Go vs Development Builds, 2024).
Identity Platform upgrade
Firebase's MFA (both TOTP and SMS) and enterprise SSO (SAML/OIDC) require upgrading from basic Firebase Auth to Google Cloud Identity Platform.
Pre-built UI vs custom flows
Clerk's three integration tiers
Tier 1 (JS-only) and Tier 2 (JS + native sign-in) are production-stable. Tier 3 (native components) is in beta as of March 2026 and should be evaluated before use in production.
1. JavaScript-only (works in Expo Go): Build custom UI with React Native components and Clerk hooks (useAuth(), useUser(), useSignIn(), useOAuth() for browser-based OAuth). Supports email/password, email codes, phone codes, and browser-based social login. Full control, no native build required.
2. JS + Native Sign-In (dev build required): Custom UI with native OAuth hooks. useSignInWithApple() (iOS only) and useSignInWithGoogle() (iOS + Android). Platform-native flows via ASAuthorization and Credential Manager. No browser redirect.
3. Native Components (Beta) (dev build required): Pre-built native UI rendered via SwiftUI on iOS and Jetpack Compose on Android. <AuthView /> handles the entire sign-in/sign-up flow, including social providers, MFA, and password recovery, without needing individual hooks or their dependencies.
import { AuthView } from '@clerk/expo/native'
import { useAuth } from '@clerk/expo'
import { useRouter } from 'expo-router'
import { useEffect } from 'react'
export default function SignInScreen() {
const { isSignedIn } = useAuth({ treatPendingAsSignedOut: false })
const router = useRouter()
useEffect(() => {
if (isSignedIn) {
router.replace('/(home)')
}
}, [isSignedIn])
return <AuthView mode="signInOrUp" />
}Clerk also provides <UserButton /> (avatar that opens a native profile modal) and <UserProfileView /> (complete profile management screen).
Firebase: build everything yourself
Firebase provides zero prebuilt UI components for React Native. FirebaseUI exists for web, iOS, and Android natively, but not React Native (Firebase Auth Documentation). Every screen, sign-in, sign-up, password reset, MFA enrollment, profile management, must be built from scratch with TextInput, TouchableOpacity, and your own state management. Independent developer tutorials confirm this gap: building a complete auth flow with Clerk requires no custom screens, while Firebase requires building every screen from scratch (DEV.to: Complete Login System with Expo and Clerk, 2025).
The tradeoff is real: pre-built components add bundle size. @clerk/expo and its dependencies are heavier than Firebase's auth-only SDK. But they also save weeks of development time. The question is whether your team would rather spend that time building auth screens or shipping product features.
Session management and token handling
Clerk
Session tokens are 60-second JWTs that auto-refresh every 50 seconds (per Clerk's architecture documentation; these are internal implementation details not independently audited). The short lifetime minimizes the window for token misuse. Tokens are cached in expo-secure-store, which uses the iOS Keychain and Android Keystore for encrypted on-device storage (expo-secure-store docs).
Session lifetimes are configurable from 5 minutes to 10 years. Core 3 introduced proactive background token refresh, so your app never waits for a mid-request refresh.
For offline scenarios, Clerk offers experimental support (not recommended as a sole production dependency for offline-first apps) via resourceCache from @clerk/expo/resource-cache. This bootstraps the app using cached resources and returns cached tokens. When the network is truly unavailable, getToken() throws a ClerkOfflineError (in Core 3). Previously it returned null, which was ambiguous with the signed-out state.
Firebase
Token behavior depends on which SDK path you chose.
Firebase JS SDK on React Native: defaults to memory-only persistence. Users get logged out every time the app closes unless you configure getReactNativePersistence with AsyncStorage (see the setup section above).
React Native Firebase: handles persistence natively via iOS Keychain and Android Keystore. No manual setup needed.
Token lifecycle (both SDKs): Firebase ID tokens expire after 1 hour. Both SDKs automatically refresh them using stored refresh tokens. The refresh happens in the background without developer intervention. Refresh tokens don't expire unless the user changes their password, the account is disabled, or tokens are explicitly revoked via the Admin SDK.
Offline behavior
Clerk's offline support is experimental and not recommended as a production dependency for offline-first apps. The resource cache bootstraps using cached resources and getToken() returns cached tokens when available.
Firebase's React Native Firebase SDK handles offline auth state natively and is production-stable. The Firebase JS SDK's Firestore offline persistence doesn't work on React Native (no IndexedDB), but auth state persists if you configured AsyncStorage.
User management and organizations
User objects compared
Clerk's user object is rich: firstName, lastName, username, multiple emails and phone numbers, metadata (public/private/unsafe), organization memberships, roles, active sessions, banned/locked status, and imageUrl with auto-generated avatars.
Firebase's user object has 5 core fields: uid, email, displayName, photoURL, emailVerified, plus phoneNumber, isAnonymous, and metadata. Custom claims are limited to 1,000 bytes. There's no built-in user search.
Organizations
Clerk has first-class organizations. Users belong to multiple orgs. Each org gets built-in roles and permissions, member invitations, verified domain auto-enrollment, and per-org enterprise SSO connections. On mobile Expo, you manage organizations using Clerk's hooks and Backend API. On Expo Web, prebuilt components like <OrganizationSwitcher /> and <OrganizationProfile /> are also available.
Firebase has no organization concept. Multi-tenancy via Identity Platform uses isolated tenant silos where a user can't belong to multiple tenants. No member management, no invitations, no org switching. Everything must be custom-built.
If you're building a team workspace, project management tool, or any multi-tenant SaaS, Clerk's org support saves months of custom development.
Expo Router integration
Both Clerk and Firebase work with Expo Router's protected route patterns. The structural patterns are similar; the difference is how auth state is provided.
Clerk + Expo Router
import { useAuth } from '@clerk/expo'
import { Redirect, Stack } from 'expo-router'
export default function HomeLayout() {
const { isSignedIn, isLoaded } = useAuth()
if (!isLoaded) return null
if (!isSignedIn) {
return <Redirect href="/(auth)/sign-in" />
}
return <Stack />
}import { useFirebaseAuth } from '@/hooks/useFirebaseAuth'
import { Stack, Redirect } from 'expo-router'
export default function AppLayout() {
const user = useFirebaseAuth()
if (user === undefined) return null // loading
if (!user) return <Redirect href="/sign-in" />
return <Stack />
}The patterns look similar. The difference is upstream: Clerk's useAuth() is backed by a managed provider with automatic token refresh. Firebase's auth state comes from onAuthStateChanged, which you wrap yourself.
Stability across Expo SDK upgrades
Firebase's compatibility history
Three consecutive Expo SDK releases (53, 54, 55) each introduced Firebase compatibility issues that required configuration changes or workarounds. Here is the incident history and current resolution state for each (all issue statuses verified March 2026):
Expo SDK 53 (April 2025): Metro's package.json exports broke the Firebase JS SDK. Multiple GitHub issues documented the fallout: expo#36588 (open, workaround documented), expo#36602 (closed, redirected), expo#36496 (closed, resolved). initializeAuth() crashed on Hermes with "INTERNAL ASSERTION FAILED: Expected a class definition" (firebase-js-sdk#9020, closed as stale). React Native Firebase's auth plugin broke with Swift AppDelegates (react-native-firebase#8487, closed, RNFB added SDK 53 support). The primary workaround: set unstable_enablePackageExports = false in Metro config. An Expo maintainer commented: "we don't maintain firebase or any integration with it." SDK 53+ also requires firebase@^12.0.0 (Expo Firebase guide).
Expo SDK 54 (September 2025): Non-modular header errors on iOS (expo#39607, closed September 2025; workaround: buildReactNativeFromSource: true). Wrong packageImportPath on Android (react-native-firebase#8761, closed as stale). Config plugin publishing regression in v23.8.0 (react-native-firebase#8829, fixed in v23.8.4+).
Expo SDK 55 (February 2026): Dropped Legacy Architecture entirely (newArchEnabled config option removed). Initial reports of iOS build errors with React Native Firebase (react-native-firebase#8908, closed March 2026 with solution provided). The fix is configuration-only: expo-build-properties with forceStaticLinking and useFrameworks: "static". No code changes to React Native Firebase were required; existing v23.8.8 works.
All known issues from SDK 53-55 have documented resolutions or workarounds (statuses verified March 2026). The recurring cost is configuration debugging during upgrades rather than blocked functionality, but it is a maintenance consideration that compounds with each Expo SDK release.
Clerk's compatibility history
@clerk/expo 3.0 requires Expo SDK 53+ and was built for it. No documented breakages with Expo SDK upgrades to date (as of March 2026). That said, @clerk/expo is a newer SDK with fewer major version transitions behind it, so the track record is shorter.
Native components use the TurboModule spec and are New Architecture native. For upgrading between Clerk SDK versions, npx @clerk/upgrade provides an automated codemod.
Using Clerk auth with a Firebase backend
A common question: can I use Clerk for auth and Firebase for the database?
The official integration (deprecated)
Clerk previously offered a built-in Firebase integration via getToken({ template: 'integration_firebase' }). This is now deprecated. New applications can't enable it. Existing apps continue to work, but once disabled, it can't be re-enabled.
The DIY pattern for new apps
The architecture looks like this:
- User authenticates with Clerk (using
@clerk/expo) - Your server endpoint verifies the Clerk session JWT
- Server uses Firebase Admin SDK to mint a Firebase custom token
- Client calls
signInWithCustomToken()from Firebase JS SDK - Firestore queries work with
request.authin security rules
Here's abbreviated server-side pseudocode:
import { clerkClient } from '@clerk/express'
import admin from 'firebase-admin'
app.post('/firebase-token', async (req, res) => {
// Verify the Clerk session
const { userId } = req.auth
// Mint a Firebase custom token using the Clerk user ID
const firebaseToken = await admin.auth().createCustomToken(userId)
res.json({ token: firebaseToken })
})The client then exchanges this token:
import { signInWithCustomToken } from 'firebase/auth'
const response = await fetch('/firebase-token', {
/* auth headers */
})
const { token } = await response.json()
await signInWithCustomToken(auth, token)Gotchas
- Firebase custom tokens are a one-time exchange credential that expires after 1 hour. After
signInWithCustomToken(), the user gets a normal Firebase session with auto-refreshing ID tokens. The custom token itself isn't the session. - Firestore offline persistence doesn't work on React Native (no IndexedDB).
- Additional latency from the server round-trip for token exchange.
Migration: Firebase to Clerk
Clerk provides an official migration path for Firebase users.
The process
Export your Firebase users via CLI:
firebase auth:export firebase-users.json --format=json --project <your-project-id>Retrieve password hash parameters from the Firebase Console (base64_signer_key, base64_salt_separator, rounds, mem_cost).
Run a migration script that POSTs to Clerk's Backend API with password_hasher: 'scrypt_firebase'. Each user is created with external_id set to the Firebase localId to preserve your existing data relationships. OAuth-only users can be imported with skip_password_requirement: true.
What to watch for
- Rate limits: Clerk's import API returns 429 responses under heavy load. Your script should handle backoff.
- Password hashing: Firebase uses modified scrypt. Clerk handles the conversion automatically during import.
- Active sessions: migrating auth in a mobile app is tricky because you can't force-update installed versions. Plan for a graceful transition period where both old and new auth work.
Migration: Clerk to Firebase
Migration works in the other direction too. Clerk doesn't lock in user data.
Export users via Clerk's Backend API (GET /v1/users with pagination). Import into Firebase using the Admin SDK's importUsers() method. Clerk uses bcrypt password hashing by default, and Firebase's importUsers() supports bcrypt via hash.algorithm: 'BCRYPT'.
OAuth-only users will need to re-link their social providers on first sign-in after migration. No official Clerk-to-Firebase migration guide exists, so this requires custom scripting.
This is a less common direction, but knowing it's possible matters when evaluating lock-in.
When to choose which
For most Expo developers starting a new project, Clerk's developer experience, Expo-first SDK, and built-in features make it the stronger choice. Firebase Auth still fits if you're deep in the Google ecosystem and primarily need basic email and social auth at scale.
Ready to try it? The Expo quickstart gets you to working auth in under 5 minutes. Or explore Clerk's Expo authentication landing page for a broader overview.