Deployment

PreviousNext

Shipping a Firebase app — deploying rules and indexes from CI with a service account, EAS builds, and the production checklist.

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.

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:

JobWhat it does
verifytsc --noEmit, lint, and the unit tests
rulesRuns firestore.rules and storage.rules against the emulator
deployPushes rules and indexes on merge to main
buildEAS 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: 21

The 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"

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:

.firebaserc
{
  "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 preview

EXPO_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

app.json
{
  "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:test passing in CI.
  • Indexes deployed and finished building.
  • Read firestore.rules line 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.

Learn more