Back to Tutorials

Firebase Auth in Expo React Native

Build a production-ready login foundation with the Firebase JS SDK, persisted auth state, Expo Router route protection, and a Firestore user profile document.

June 16, 2026 13 min read Paddy B
ExpoReact NativeFirebase AuthExpo RouterFirestore

The short version

Use the Firebase JS SDK for managed Expo apps, initialize Auth with React Native persistence, expose auth state through a provider, protect routes with Expo Router, and create a Firestore profile document the first time a user signs up.

Firebase Auth is a good fit for Expo when you want email/password, magic links, Google or Apple sign-in, and backend security rules that can read request.auth.uid. The key is treating the mobile app as a client: it can hold public Firebase config, but it should never hold admin credentials.

1. Install the right packages

Start with the Firebase JS SDK and AsyncStorage. AsyncStorage is what keeps the user signed in after the app process is killed.

npx expo install firebase @react-native-async-storage/async-storage

If you need native-only Firebase features such as Crashlytics or Dynamic Links, look at React Native Firebase and a development build. For Auth and Firestore, the JS SDK is usually the simplest Expo path.

2. Put Firebase config in Expo env vars

Firebase web config values are client identifiers, not server secrets. Use EXPO_PUBLIC_ variables so Expo can inline them into the app bundle.

EXPO_PUBLIC_FIREBASE_API_KEY=... EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com EXPO_PUBLIC_FIREBASE_PROJECT_ID=your-project EXPO_PUBLIC_FIREBASE_APP_ID=...

Keep service account JSON, Admin SDK credentials, private keys, SMTP passwords, and webhook secrets out of the app. Those belong in Cloud Functions, your server, or EAS secret variables for build-time tasks only.

3. Initialize Firebase once

Create a single Firebase module. The important bit for React Native is initializeAuth with AsyncStorage persistence.

import AsyncStorage from "@react-native-async-storage/async-storage"; import { initializeApp, getApps } from "firebase/app"; import { getFirestore } from "firebase/firestore"; import { getReactNativePersistence, initializeAuth } from "firebase/auth"; const firebaseConfig = { apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY, authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN, projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID, appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID, }; export const app = getApps().length ? getApps()[0] : initializeApp(firebaseConfig); export const auth = initializeAuth(app, { persistence: getReactNativePersistence(AsyncStorage), }); export const db = getFirestore(app);

If Fast Refresh complains that Auth is already initialized, wrap the Auth creation in a helper that falls back to getAuth(app) when an instance already exists.

4. Create sign-up and sign-in functions

Keep the UI thin. Put auth calls in a service module so screens can handle loading, validation, and errors without knowing the Firebase details.

import { createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut } from "firebase/auth"; import { doc, serverTimestamp, setDoc } from "firebase/firestore"; import { auth, db } from "./firebase"; export async function registerWithEmail(email: string, password: string) { const credential = await createUserWithEmailAndPassword(auth, email, password); await setDoc(doc(db, "users", credential.user.uid), { email: credential.user.email, createdAt: serverTimestamp(), plan: "free", }); return credential.user; } export function loginWithEmail(email: string, password: string) { return signInWithEmailAndPassword(auth, email, password); } export function logout() { return signOut(auth); }

Store product-specific profile data in Firestore, not in Firebase Auth custom claims unless your rules or backend truly need it. Claims are powerful, but they are also slower to update and easy to overuse.

5. Protect routes with Expo Router

Subscribe to onAuthStateChanged once in a provider. Then redirect based on the current auth state and a loading flag.

import { onAuthStateChanged, User } from "firebase/auth"; import { createContext, useContext, useEffect, useState } from "react"; import { auth } from "./firebase"; const AuthContext = createContext({ user: null as User | null, loading: true }); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { return onAuthStateChanged(auth, (nextUser) => { setUser(nextUser); setLoading(false); }); }, []); return <AuthContext.Provider value={{ user, loading }}>{children}</AuthContext.Provider>; } export function useAuth() { return useContext(AuthContext); }

In a protected layout, render nothing while loading, redirect signed-out users to /(auth)/login, and let signed-in users continue into the app. That avoids flashing private screens during startup.

6. Write rules around uid ownership

Your first profile rule should be boring: users can read and write their own document, and nobody else's.

rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId} { allow read, update, delete: if request.auth != null && request.auth.uid == userId; allow create: if request.auth != null && request.auth.uid == userId; } } }

Do not rely on hidden screens for security. If a client can connect to Firestore, rules are the security boundary.

Production checklist

  • Enable only the sign-in providers you actually use in Firebase Console.
  • Use real Firestore rules before public testing.
  • Handle common Auth errors without leaking whether an email belongs to an account.
  • Add account deletion and data export paths if your product stores personal data.
  • Test fresh install, app restart, token expiry, logout, and offline startup.