- 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
Two things ship separately: your rules and indexes go to Firebase, and your app goes to the stores through EAS.
Deploying rules and indexes
pnpm deploy:rules # firestore.rules + storage.rules npm run deploy:indexes # firestore.indexes.json npm run deploy # both
Rules and indexes are code. Deploy them from CI on merge, not from a laptop — editing rules in the console means your repository and your production configuration disagree, and nothing tells you.
A client rolled out with a query whose index is missing fails for every user at once. Deploy rules and indexes first, then submit the build.
Indexes take time to build
A new composite index on an existing collection is not instant — Firestore
backfills it, which on a large collection can take minutes to hours. Queries
that need it fail with failed-precondition until it is ready. Check progress
under Firestore → Indexes.
CI
Both starters ship .github/workflows/ci.yml with four jobs:
| Job | What it does |
|---|---|
verify | tsc --noEmit, lint, and the unit tests |
rules | Runs firestore.rules and storage.rules against the emulator |
deploy | Pushes rules and indexes on merge to main |
build | EAS build |
The rules job is the one worth keeping. Everything else runs against mocks or
the client SDK's local cache, both of which accept writes the server would
reject.
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21The emulators are Java processes, and firebase-tools 15 requires JDK 21 or newer — it refuses to start on anything older.
Authenticating the deploy
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ github.workspace }}/service-account.json
steps:
- name: Write the service account key
run: echo '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}' > "$GOOGLE_APPLICATION_CREDENTIALS"
- run: npx firebase-tools deploy --only firestore,storage --project "${{ secrets.FIREBASE_PROJECT_ID }}"
- name: Remove the key
if: always()
run: rm -f "$GOOGLE_APPLICATION_CREDENTIALS"firebase login:ci tokens are deprecated in firebase-tools 13 and later, and
most tutorials still show them. Use a service account: IAM → Service accounts
→ create one with the Firebase Admin role, download the JSON key, and
paste it whole into a repository secret.
That key grants full admin access to your project and bypasses every rule in
firestore.rules. The if: always() cleanup step matters — a private key
written to a runner outlives the step that wrote it otherwise. It must never be
committed, and .gitignore blocks service-account*.json for that reason.
Environments
The simplest split is two Firebase projects — my-app-dev and my-app-prod —
with .firebaserc naming both:
{
"projects": {
"default": "my-app-dev",
"production": "my-app-prod"
}
}pnpm dlx firebase-tools deploy --only firestore,storage --project production
The app picks its project from EXPO_PUBLIC_FIREBASE_*, so the corresponding
split lives on the EAS build profile.
EAS builds
eas build --platform all --profile previewEXPO_PUBLIC_ variables are inlined into the bundle at build time, so they
must be set where the build runs — on the EAS build profile in eas.json, or as
EAS environment variables. Setting them only in CI's shell environment produces
a build with an undefined Firebase config, which fails at the first import with
the error lib/firebase.ts throws.
Before your first build
{
"ios": { "bundleIdentifier": "com.yourcompany.myapp" },
"android": { "package": "com.yourcompany.myapp" }
}Not in the scaffold, because a placeholder breaks the build. The Google iOS and Android OAuth clients are tied to these values, so set them before creating those clients.
Signing keys and Google sign-in
An Android OAuth client is bound to a package name and a signing SHA-1. A development build signed with the debug keystore and a preview build signed with the EAS keystore are different fingerprints, so each needs its own client.
This is why Google sign-in commonly works locally and fails in a preview build.
eas credentials prints the SHA-1 for each profile.
Production checklist
- Rules deployed, and
npm run rules:testpassing in CI. - Indexes deployed and finished building.
- Read
firestore.rulesline by line. On the no-auth starter it is open — anyone with the app can read and delete everything. - Confirm there is no
match /{document=**}. - Decide whether email verification is mandatory. It is not by default.
- Set a real password policy if you rely on the client-side rules.
- Enable App Check if abuse is a concern — it attests that requests come from your app rather than a script with your config.
- Set a budget alert. Firestore bills per document read, and a listener
without
limit()on a growing collection is the usual surprise. - If you offer Google sign-in on iOS, ship Apple sign-in too. App Store Review Guideline 4.8.
- Confirm no service account JSON is in the repository.
What this starter does not deploy
Cloud Functions. They require the paid Blaze plan, and a starter should not force a billing account. The two places you would reach for one:
- Creating the profile document. Handled client-side from the provider's snapshot listener, which self-heals.
- Cascading deletes on account deletion. Handled best-effort on the device. Firebase's official "Delete User Data" extension is the robust version.
Both are documented in Authentication.