- Accordion
- Action Sheet
- Alert Dialog
- Alert
- Audio Player
- Audio Recorder
- Audio Waveform
- Avatar
- AvoidKeyboard
- Badge
- BottomSheet
- Button
- Camera Preview
- Camera
- Card
- Carousel
- Checkbox
- Collapsible
- Color Picker
- Combobox
- Date Picker
- File Picker
- Gallery
- Hello Wave
- Icon
- Image
- Input OTP
- Input
- Link
- MediaPicker
- Mode Toggle
- Onboarding
- ParallaxScrollView
- Picker
- Popover
- Progress
- Radio
- ScrollView
- SearchBar
- Separator
- Share
- Sheet
- Skeleton
- Spinner
- Switch
- Table
- Tabs
- Text
- Toast
- Toggle
- Video
- View
Firebase is Google's app platform: a document database that streams changes to every listener, object storage, an authentication service with a dozen providers, and a rules language that runs on Google's servers rather than in your app.
BNA UI ships two Firebase scaffolds: one with a backend and no sign-in, one with
authentication and the screens already built. Both use the firebase JS
SDK, so they run in Expo Go — no config plugin, no google-services.json, no
development build to get started.
Firestore, Cloud Storage, rules and rules tests. No sign-in.
Password, Google and Apple, with owner-scoped documents.
The auth starter
Built around the same principles as the rest of BNA UI:
- Open Code: the authentication screens, the hooks and the security rules are copied into your project, not imported from a package.
- Mobile-First: flows designed for React Native, with the persisted user encrypted in the platform keychain.
- Cross-Platform: iOS, Android and web, from one code path.
- Secure by default: every document scoped to
request.auth.uid, enforced by Google — not by what the client asks for.
Sign-in methods
| Method | Mechanism | Ships with a screen | Expo Go |
|---|---|---|---|
| Email + password | signInWithEmailAndPassword | Yes | Yes |
| Password reset | sendPasswordResetEmail | Yes | Yes |
| Email verification | sendEmailVerification | Card in Settings | Yes |
expo-auth-session → signInWithCredential | Yes | No | |
| Apple | expo-apple-authentication → signInWithCredential | Yes | No |
| Email link | sendSignInLinkToEmail | Yes, hidden by default | No |
There is no email OTP — Firebase Authentication has no six-digit email code at all, only SMS. And there is no GitHub: obtaining a GitHub access token requires a client secret, which cannot ship in an app bundle. Supabase does that exchange on its own servers; Firebase expects you to run one.
What lands in your project
lib/
├── firebase.ts the client — app, auth, Firestore, Storage
├── large-secure-store.ts AES-256 persistence for the auth record
├── documents.ts snapshot → plain object, as pure functions
├── auth-link.ts the action-link parser, as a pure function
└── errors.ts Firebase error code → prose
providers/auth-provider.tsx user, profile, deep links
app/(auth)/ five screens
app/(onboarding)/ intro carousel + profile setup
firestore.rules owner-only, with no catch-all match
storage.rules avatars/<uid>/… and files/<uid>/…
rules-tests/ the rules, executed against the emulator
The two layers of access control
The route guards in app/_layout.tsx decide what renders:
<Stack.Protected guard={!signedIn}>
<Stack.Screen name='(auth)' />
</Stack.Protected>
<Stack.Protected guard={signedIn && !needsOnboarding}>
<Stack.Screen name='(tabs)' />
</Stack.Protected>The security rules decide what Firestore will actually return:
match /tasks/{taskId} {
allow read: if isOwner(resource.data.ownerId);
}
Only the second is a security boundary. The first is there so users are not looking at empty screens.
The rule that catches everyone
Firestore evaluates a read rule against the query, not against the documents it would return. It refuses any query it cannot prove in advance is limited to documents the rule allows:
// permission-denied, even signed in
query(collection(db, 'tasks'), orderBy('createdAt', 'desc'));
// fine
query(collection(db, 'tasks'), where('ownerId', '==', uid));A Postgres RLS policy does the opposite: the filter is optional and the policy quietly narrows the result. If you are moving between the Supabase and Firebase starters, this is the difference that will bite you. Security rules covers it properly.
Reading the signed-in user
import { useAuth } from '@/providers/auth-provider';
const { user, profile, loading, signOut } = useAuth();profile is the users/{uid} document, kept current over an onSnapshot
subscription. user is the Firebase User record. There is no session —
Firebase has no session object.
Environment
| Variable | Where it lives | Set by | Used for |
|---|---|---|---|
EXPO_PUBLIC_FIREBASE_API_KEY | .env.local | bna-ui firebase | Building the client |
EXPO_PUBLIC_FIREBASE_PROJECT_ID | .env.local | bna-ui firebase | Building the client |
EXPO_PUBLIC_FIREBASE_APP_ID | .env.local | bna-ui firebase | Building the client |
EXPO_PUBLIC_GOOGLE_*_CLIENT_ID | .env.local | You | Google sign-in |
EXPO_PUBLIC_FIREBASE_LINK_URL | .env.local | You | Email links |
GOOGLE_APPLICATION_CREDENTIALS | CI | You | Deploying rules |
Unlike a Supabase key, these values identify your project rather than granting
access to it — Google publishes them in the console for you to paste into a
web page. What actually guards your data is firestore.rules and
storage.rules. What must never appear in .env.local is a service account
JSON, an Admin SDK private key, or an OAuth client secret.
Provider credentials — the Apple team key, SMTP settings, OAuth secrets — live in the Firebase console, not in any file in your repository.
Guides
- Firestore — the data model, queries, indexes and
what
serverTimestamp()does locally - Security rules — the query-scoping rule, the Storage
deletetrap, and how to test both - Authentication — persistence, route guards, deep links and account deletion
- Google · Apple · Email and links
- Realtime · Storage
- Deployment — EAS, CI/CD, and the production checklist
- Troubleshooting — and a Supabase → Firebase migration guide