A production-ready React Native + Expo starter template for building SaaS applications. Clone, configure, and ship.
- Expo Router - File-based routing with route groups
- Firebase Auth - Email/password authentication
- NativeWind - Tailwind CSS for React Native
- TanStack Query - Data fetching and caching
- TypeScript - Full type safety
- Dark Mode - Automatic system preference support
- Onboarding - Multi-step onboarding flow
- Navigation Guard - Centralized routing logic
- UI Primitives - Reusable components (Button, Input, Screen, Loader)
- React Native (Expo)
- Expo Router (file-based routing)
- Firebase (Auth + Firestore)
- NativeWind (Tailwind-style styling)
- TanStack Query (data fetching & caching)
- TypeScript (strict mode)
├── app/
│ ├── _layout.tsx # Root layout with providers
│ ├── +not-found.tsx # 404 screen
│ ├── (public)/ # Public routes
│ │ ├── onboarding.tsx
│ │ ├── login.tsx
│ │ └── signup.tsx
│ └── (protected)/ # Protected routes
│ ├── index.tsx # Home
│ ├── profile.tsx
│ └── settings.tsx
│
├── src/
│ ├── components/ui/ # UI primitives
│ ├── providers/ # Context providers
│ ├── hooks/ # Custom hooks
│ ├── services/ # Firebase, storage
│ ├── lib/ # Query client config
│ └── types/ # TypeScript types
│
├── tailwind.config.js # Design tokens
└── app.json # Expo configuration
git clone <your-repo-url>
cd expo-saas-starternpm install- Create a new Firebase project at Firebase Console
- Enable Email/Password authentication in Firebase Console
- Create a Firestore database
- Copy your Firebase config
cp .env.example .envEdit .env and add your Firebase credentials:
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_idEdit app.json:
- Change
nameto your app name - Change
slugto your app slug - Update
bundleIdentifier(iOS) andpackage(Android) - Update
schemefor deep linking
npm startThen press:
ifor iOS simulatorafor Android emulatorwfor web
- Splash Screen - Shows while checking auth state
- Onboarding - Multi-step onboarding (if not completed)
- Auth Screens - Login/Signup (if not authenticated)
- App Screens - Protected home screen (if authenticated)
The NavigationGuard component handles all routing logic:
- If onboarding NOT completed → show onboarding
- If onboarding completed but NOT logged in → show auth screens
- If logged in → show protected app screens
This logic is centralized in src/providers/NavigationGuard.tsx.
Edit tailwind.config.js to update your color scheme:
colors: {
primary: {
// Your brand colors
},
}Edit app/(public)/onboarding.tsx to customize onboarding steps.
Edit app/(protected)/_layout.tsx to add/remove tabs.
All UI primitives are in src/components/ui/:
Button.tsx- Primary, secondary, outline variantsInput.tsx- Text input with label and error statesScreen.tsx- Safe area wrapper with scroll supportLoader.tsx- Loading indicator
Install an icon library:
npx expo install @expo/vector-iconsUpdate the TabIcon component in app/(protected)/_layout.tsx.
Example usage pattern:
import { useQuery } from '@tanstack/react-query';
import { collection, getDocs } from 'firebase/firestore';
import { db } from '@/services/firebase';
export function useItems() {
return useQuery({
queryKey: ['items'],
queryFn: async () => {
const snapshot = await getDocs(collection(db, 'items'));
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
},
});
}Don't forget to set up Firestore security rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}- Configure
app.jsonwith your bundle identifier - Run:
npx expo run:ios --configuration Release
- Configure
app.jsonwith your package name - Run:
npx expo run:android --variant release
npm install -g eas-cli
eas login
eas build --platform allnpm start- Start Expo dev servernpm run ios- Start iOS simulatornpm run android- Start Android emulatornpm run web- Start web developmentnpm run lint- Run ESLintnpm run type-check- Run TypeScript type checking
Route groups (public) and (protected) allow for:
- Different layouts for auth vs app screens
- Clean URL structure
- Easy navigation guard implementation
- Zero backend setup
- Built-in auth
- Real-time database
- Easy scaling
- Familiar Tailwind syntax
- No StyleSheet boilerplate
- Easy dark mode support
- Consistent spacing/colors
- Automatic caching
- Background refetching
- Optimistic updates
- Less boilerplate than Redux
import { clearOnboardingStatus } from '@/services/storage';
await clearOnboardingStatus();- Create file in
app/(protected)/orapp/(public)/ - Use the
Screencomponent as wrapper - Add to navigation if needed
Edit src/services/firebase.ts and src/hooks/useAuth.tsx to add:
- Google Sign-In
- Apple Sign-In
- Phone Auth
- etc.
npx expo start -cnpm run type-checkrm -rf node_modules .expo
npm install- Update Firebase config with production credentials
- Set up Firestore security rules
- Update app.json with correct bundle IDs
- Add app icons and splash screen
- Test on both iOS and Android
- Set up error tracking (Sentry, etc.)
- Configure analytics
- Test offline behavior
- Review and optimize bundle size
MIT
For issues and questions, please open a GitHub issue.
Built with speed and scalability in mind. Clone, configure, ship.