react-native-fireguard
v1.0.3
Published
The easiest way to add secure Firebase authentication, protected routes, and role-based access control to your Expo React Native apps.
Maintainers
Readme
react-native-fireguard 🔥🛡️
The easiest way to add secure Firebase authentication, protected routes, and role-based access control to your Expo React Native apps. Think Clerk — but for Firebase.
Why FireGuard?
Building auth in React Native with Firebase is repetitive, error-prone, and leaves security gaps. FireGuard solves this with a single package that gives you:
- ✅ Automatic route protection — unauthenticated users are redirected immediately
- ✅ Role-based access control — show/hide content based on user roles from Firestore
- ✅ Declarative components —
<SignedIn>,<SignedOut>,<Protect>just like Clerk - ✅ Pre-built auth screens — drop-in
<SignIn>and<SignUp>components - ✅ Security-first — tokens never stored in state, server-side revocation detection
- ✅ Expo Router ready — works seamlessly with file-based routing
Installation
npm install react-native-fireguardInstall peer dependencies
npx expo install firebase @react-native-async-storage/async-storage expo-routerQuick Start
1. Wrap your root layout
// app/_layout.jsx
import { Slot } from "expo-router";
import { FirebaseAuthProvider, RouteGuard } from "react-native-fireguard";
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,
storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
};
export default function RootLayout() {
return (
<FirebaseAuthProvider config={firebaseConfig}>
<RouteGuard
loginRoute="/(auth)/login"
homeRoute="/(tabs)"
authSegment="(auth)"
adminSegment="(admin)"
>
<Slot />
</RouteGuard>
</FirebaseAuthProvider>
);
}2. Add a login screen
// app/(auth)/login.jsx
import { SignIn } from "react-native-fireguard";
export default function LoginScreen() {
return <SignIn />;
}3. Protect your screens
// app/(tabs)/home.jsx
import { View, Text, TouchableOpacity } from "react-native";
import { SignedIn, SignedOut, Protect, useAuth } from "react-native-fireguard";
export default function HomeScreen() {
const { user, role, signOut } = useAuth();
return (
<View>
<SignedIn>
<Text>Welcome, {user?.email}!</Text>
<Text>Role: {role}</Text>
{/* Only visible to admins */}
<Protect role="admin">
<Text>🔐 Admin-only content</Text>
</Protect>
{/* Multiple roles */}
<Protect role={["admin", "moderator"]}>
<Text>Visible to admins and moderators</Text>
</Protect>
<TouchableOpacity onPress={signOut}>
<Text>Sign Out</Text>
</TouchableOpacity>
</SignedIn>
<SignedOut>
<Text>Please sign in to continue</Text>
</SignedOut>
</View>
);
}Folder Structure
FireGuard works best with this Expo Router folder structure:
app/
├── _layout.jsx ← FirebaseAuthProvider + RouteGuard
├── (auth)/
│ ├── _layout.jsx ← Public screens (no guard)
│ └── login.jsx ← Login/signup screen
├── (tabs)/
│ ├── _layout.jsx ← Protected tab navigator
│ ├── home.jsx ← Protected home screen
│ ├── profile.jsx ← Protected profile screen
│ └── admin.jsx ← Admin-only tab (hidden for regular users)
└── (admin)/
├── _layout.jsx ← Admin stack navigator
└── dashboard.jsx ← Full admin dashboardAPI Reference
<FirebaseAuthProvider>
The root provider. Wrap your entire app with this.
| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| config | FirebaseOptions | ✅ | — | Your Firebase config object |
| roleCollection | string | ❌ | "users" | Firestore collection name for user roles |
| requireEmailVerification | boolean | ❌ | false | Treat unverified emails as signed out |
| onTokenError | (error: Error) => void | ❌ | — | Called when token refresh fails |
| onSignOut | () => void | ❌ | — | Called after sign out |
<FirebaseAuthProvider
config={firebaseConfig}
roleCollection="users"
requireEmailVerification={false}
onTokenError={(err) => console.warn('Token error:', err.message)}
onSignOut={() => console.log('User signed out')}
>
...
</FirebaseAuthProvider><RouteGuard>
Automatically redirects unauthenticated users to your login screen and authenticated users away from auth screens.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| loginRoute | string | "/(auth)/login" | Where to redirect unauthenticated users |
| homeRoute | string | "/(tabs)" | Where to redirect authenticated users on auth screens |
| authSegment | string | "(auth)" | Expo Router group name for auth screens |
| adminSegment | string | "(admin)" | Expo Router group name for admin screens |
<RouteGuard
loginRoute="/(auth)/login"
homeRoute="/(tabs)"
authSegment="(auth)"
adminSegment="(admin)"
>
<Slot />
</RouteGuard><SignedIn>
Renders children only when the user is authenticated.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Content to show when signed in |
| fallback | ReactNode | null | Content to show while auth is loading |
<SignedIn fallback={<ActivityIndicator />}>
<Text>Welcome back!</Text>
</SignedIn><SignedOut>
Renders children only when the user is NOT authenticated.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| children | ReactNode | — | Content to show when signed out |
| fallback | ReactNode | null | Content to show while auth is loading |
<SignedOut>
<Text>Please log in to continue</Text>
</SignedOut><Protect>
Renders children only for users with the specified role. Shows a fallback for unauthorized users.
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| role | string \| string[] | — | Required role(s) to view content |
| condition | (role: string \| null) => boolean | — | Custom permission function |
| fallback | ReactNode | <Text>Access Denied</Text> | Shown when access is denied |
| loadingFallback | ReactNode | null | Shown while auth loads |
// Single role
<Protect role="admin">
<AdminPanel />
</Protect>
// Multiple roles
<Protect role={["admin", "moderator"]} fallback={<Text>No access</Text>}>
<ModeratorTools />
</Protect>
// Custom condition
<Protect condition={(role) => role === "admin" && isPremiumUser}>
<PremiumAdminFeature />
</Protect><AuthLoading>
Renders children ONLY while auth state is being determined. Useful for splash screens.
<AuthLoading>
<SplashScreen />
</AuthLoading><SignIn>
Pre-built sign-in form with validation, loading states, and error handling.
| Prop | Type | Description |
|------|------|-------------|
| onSuccess | () => void | Called after successful sign in |
<SignIn onSuccess={() => router.replace("/(tabs)")} /><SignUp>
Pre-built sign-up form.
| Prop | Type | Description |
|------|------|-------------|
| onSuccess | () => void | Called after successful sign up |
<SignUp onSuccess={() => router.replace("/(tabs)")} />useAuth()
The main hook. Access auth state from any component inside <FirebaseAuthProvider>.
const {
user, // SafeUser | null — sanitized user object
role, // string | null — user's role from Firestore
isLoaded, // boolean — true once auth state is confirmed
isSignedIn, // boolean — true if user is authenticated (derived)
signOut, // () => Promise<void>
getIdToken, // (forceRefresh?: boolean) => Promise<string | null>
} = useAuth();user object properties
{
uid: string;
email: string | null;
displayName: string | null;
photoURL: string | null;
emailVerified: boolean;
isAnonymous: boolean;
phoneNumber: string | null;
role: string; // fetched from Firestore
}getIdToken(forceRefresh?)
Fetches a fresh Firebase ID token for use in API calls. Always use this instead of caching tokens yourself.
const { getIdToken } = useAuth();
// Standard API call
const token = await getIdToken();
// Force refresh before sensitive operations (payments, account changes)
const freshToken = await getIdToken(true);
const response = await fetch("https://api.yourapp.com/data", {
headers: { Authorization: `Bearer ${token}` },
});getFriendlyAuthError(errorCode)
Converts Firebase Auth error codes into user-friendly messages. Prevents email enumeration by returning the same message for user-not-found and wrong-password.
import { getFriendlyAuthError } from "react-native-fireguard";
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (e) {
Alert.alert("Error", getFriendlyAuthError(e.code));
}Role-Based Access Control (RBAC)
FireGuard reads user roles from Firestore. Set up your roles like this:
Firestore structure
Collection: users
└── Document: {firebase_auth_uid} ← Document ID must be the user's UID
├── role: "admin" ← string field
├── email: "[email protected]"
└── createdAt: "2026-01-01T00:00:00"Setting up roles
Step 1 — Get the user's UID:
Firebase Console → Authentication → Users → copy the User UIDStep 2 — Create the Firestore document:
Firebase Console → Firestore Database → users collection
→ Add document
→ Document ID: {paste the UID}
→ Add field: role (string) = "admin"Available roles
FireGuard doesn't enforce specific role names — you define them. Common patterns:
// Simple two-tier
<Protect role="admin">...</Protect>
// Three-tier
<Protect role={["admin", "moderator"]}>...</Protect>
// Custom roles
<Protect role="premium">...</Protect>
<Protect role={["editor", "publisher", "admin"]}>...</Protect>Admin-only tab (hidden from regular users)
// app/(tabs)/_layout.jsx
import { Tabs } from "expo-router";
import { useAuth } from "react-native-fireguard";
export default function TabsLayout() {
const { role } = useAuth();
const isAdmin = role === "admin" || role === "moderator";
return (
<Tabs>
<Tabs.Screen name="home" options={{ title: "Home" }} />
<Tabs.Screen name="profile" options={{ title: "Profile" }} />
<Tabs.Screen
name="admin"
options={{
title: "Admin",
href: isAdmin ? undefined : null, // hidden for non-admins
}}
/>
</Tabs>
);
}Environment Variables
Never hardcode Firebase config. Use Expo's environment variables:
# .env (add to .gitignore!)
EXPO_PUBLIC_FIREBASE_API_KEY=your_api_key
EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
EXPO_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET=your_project.appspot.com
EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
EXPO_PUBLIC_FIREBASE_APP_ID=your_app_idconst 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,
storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
};Firestore Security Rules
Protect your users collection so only authenticated users can read their own role, and only backend/admin can write:
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
// Users can only read their own document
allow read: if request.auth != null && request.auth.uid == userId;
// Only Firebase Admin SDK (backend) can write roles
allow write: if false;
}
}
}Security Notes
FireGuard is built with security as the top priority:
- Tokens never stored in React state — the raw Firebase
Userobject is kept in auseRef, invisible to React DevTools onIdTokenChangedinstead ofonAuthStateChanged— catches server-side token revocations thatonAuthStateChangedmisses- Email enumeration prevention —
user-not-foundandwrong-passwordreturn the same error message - Auto sign-out on token failure — if a token refresh fails, the user is signed out immediately
- Role fetch timeout — Firestore role fetch has a 5-second timeout; defaults to
"user"if Firestore is unreachable
Peer Dependencies
| Package | Version |
|---------|---------|
| firebase | >=9.0.0 |
| expo-router | >=2.0.0 |
| @react-native-async-storage/async-storage | >=1.0.0 |
| react | >=18.0.0 |
| react-native | >=0.71.0 |
Compatibility
| Expo SDK | React Native | Status | |----------|-------------|--------| | 54 | 0.81 | ✅ Tested | | 52+ | 0.76+ | ✅ Should work | | Below 52 | Below 0.76 | ⚠️ Not tested |
Troubleshooting
"useAuth must be used within FirebaseAuthProvider"
Ensure <FirebaseAuthProvider> wraps your root layout, not a group layout.
Role always shows as "user" even after setting admin in Firestore
- Check the document ID matches the Firebase Auth UID exactly
- Check the
roleCollectionprop matches your Firestore collection name (default:"users") - Sign out and sign back in to refresh the role
RouteGuard not redirecting
- Make sure
<RouteGuard>is inside<FirebaseAuthProvider> - Check
authSegmentmatches your Expo Router group name exactly (e.g.,"(auth)") - Ensure
loginRouteis a valid Expo Router path
getReactNativePersistence error
Make sure you're using Firebase v9+ and have @react-native-async-storage/async-storage installed.
Contributing
Contributions are welcome! Please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Repository
https://github.com/Inspire00/react-native-fireguard
License
MIT © Vincent
Changelog
1.0.0 (2026-05-10)
- 🎉 Initial release
FirebaseAuthProviderwith Firestore role supportRouteGuardfor automatic route protectionSignedIn,SignedOut,Protect,AuthLoadingcomponentsSignIn,SignUppre-built screensuseAuthhook withgetIdTokensupport- Email enumeration prevention
- Server-side token revocation detection
