← Build Firebase for React Native

Expo · React Native · September 2026

Firebase for React Native

Firebase is not one product — it's about eighteen separate Google-run services sharing a console and a config file. In React Native there's a second fork nobody warns you about: two entirely different SDKs claim to be "Firebase", and picking the wrong one costs you half the catalogue. This page covers both, and every service, in modular TypeScript.

React Native Firebase 26.4.0 Expo SDK 54 RN 0.81 API style modular only

Two SDKs both called Firebase

Get this wrong and you'll spend a day wondering why Crashlytics reports nothing.

There are two ways to talk to Firebase from React Native, and they are not interchangeable. Most tutorials don't say which one they're using, which is why so much Firebase-in-RN advice silently fails.

 React Native FirebaseFirebase JS SDK
Package@react-native-firebase/*firebase
What it isTurboModule wrappers around the real native Android/iOS SDKsThe web SDK, running in your JS engine over HTTP and WebSocket
Needs a native buildYes — dev build or EAS buildNo, works in Expo Go
Analytics, Crashlytics, PerformanceYesImpossible — these are native-only
Push (FCM)YesWeb push only, not mobile
Firestore offline cacheFull native persistenceLimited; memory-first
SQL ConnectNo moduleYes

For a production mobile app, React Native Firebase is the answer. The three products with the best effort-to-value ratio in all of Firebase — Crashlytics, Analytics, and Cloud Messaging — are native-only and simply do not exist in the JS SDK. There is no workaround; they instrument the OS, not your JavaScript.

The JS SDK earns its place in exactly two situations: a React Native Web build that also has to run in a browser, and SQL Connect, which has no native module. You can install both side by side in one app if you need to — they're separate packages reading the same project config — but keep one library per product, never two clients for the same Firestore.

Version 26 removed the namespaced API entirely. If you find code like firebase.analytics().logEvent(...) or auth().currentUser, it is pre-v22 and will not run. Everything below is modular: import the function, pass the instance in as the first argument. Every snippet on this page is v26 style.

How it actually runs

Four layers. Knowing where each one stops explains most Firebase bugs in React Native.

Your JavaScript never talks to Google. It calls a TurboModule, which calls the real Firebase SDK compiled into your app binary, which talks to Google. That indirection is the source of nearly every confusing symptom in RN Firebase — so it's worth holding in your head:

Your JS / TS telemetry.ts stores, screens hot-reloads RNFirebase TurboModules JS ↔ native bridge in the bundle Native SDK compiled into the .apk / .ipa needs a rebuild Google Firestore, GA4, Crashlytics, FCM security rules Expo Go ships everything on the left, and nothing on the right JS / NATIVE
Adding a Firebase package changes layers 2 and 3, so it needs a native rebuild — npx expo run:android or an EAS build. Changing how you call it only changes layer 1, so Fast Refresh is enough.

Three consequences fall straight out of that picture:

  • Expo Go will never work. It's a pre-built binary containing a fixed set of native modules, and the Firebase SDKs aren't among them. Your calls hit a missing module and do nothing. This is not a bug and there's no flag for it.
  • Adding a Firebase package means a rebuild. npm install plus Fast Refresh is not enough; new native code has to be compiled in.
  • Authorisation lives server-side. Your JS bundle ships to strangers' phones and can be read. Firestore security rules, Cloud Functions and App Check decide what's allowed — never a check in your React code.

One more grouping worth knowing, because it predicts cost and offline behaviour. Data services (Firestore, Realtime DB, Storage) are two-way round trips billed per operation and cached on device. Engagement services (Messaging, Remote Config, In-App Messaging) push down to the device and are free. Telemetry services (Crashlytics, Analytics, Performance) only ever send upward, are free, and nothing comes back.

Wiring it up in Expo

Config plugins, not manual Gradle edits. Do this once and adding the next module is two lines.

With Expo's Continuous Native Generation, you don't hand-edit build.gradle or the Podfile — you declare config plugins in app.json and let prebuild write the native code. Every RNFirebase module ships one.

Install — always via expo install, so versions track the SDK
npx expo install @react-native-firebase/app \
  @react-native-firebase/analytics \
  @react-native-firebase/crashlytics \
  @react-native-firebase/messaging \
  @react-native-firebase/remote-config
app.json
{
  "expo": {
    "android": {
      "package": "com.example.app",
      // points at the file you downloaded from the console
      "googleServicesFile": "./google-services.json"
    },
    "ios": {
      "googleServicesFile": "./GoogleService-Info.plist"
    },
    "plugins": [
      "@react-native-firebase/app",          // must come first
      "@react-native-firebase/analytics",
      "@react-native-firebase/crashlytics",
      ["expo-build-properties", {
        "ios": { "useFrameworks": "static" }   // RNFirebase requires this on iOS
      }]
    ]
  }
}

One more file, easy to miss, and it's the reason people think Crashlytics is broken locally:

firebase.json — RNFirebase's own flags, at the project root
{
  "react-native": {
    // without this, Crashlytics is a no-op in debug builds
    "crashlytics_debug_enabled": true,
    "crashlytics_auto_collection_enabled": true,

    // useful when you need consent before any collection starts
    "analytics_auto_collection_enabled": true,
    "messaging_auto_init_enabled": true
  }
}

Then build. npx expo run:android compiles a dev client locally; eas build --profile development does it in the cloud. Either way you install that binary once and Metro serves your JS into it exactly like Expo Go — you only rebuild when native dependencies change.

There is no initializeApp() call. The native SDK reads google-services.json at process start, before your JS bundle even loads. So getAnalytics(), getMessaging() and the rest just work at module scope — no async init, no provider component to wrap your tree in.

One wrapper module, always

Before you scatter Firebase imports through your screens: put every @react-native-firebase/* import behind a single module of your own. It takes ten minutes and pays for itself three times — your tests mock one file instead of five packages, telemetry failures can be swallowed in one place (an analytics call must never break a user flow), and swapping or removing a product later is one file's work.

src/lib/telemetry.ts — the only file that imports Firebase
import { getAnalytics, logEvent as fbLogEvent } from '@react-native-firebase/analytics';
import { getCrashlytics, recordError as fbRecordError } from '@react-native-firebase/crashlytics';

const analytics = getAnalytics();
const crashlytics = getCrashlytics();

export function logEvent(name: string, params?: Record<string, unknown>): void {
  try { void fbLogEvent(analytics, name, params); } catch { /* never throw */ }
}

export function recordError(e: unknown): void {
  try { fbRecordError(crashlytics, e instanceof Error ? e : new Error(String(e))); } catch {}
}

Telemetry

Free, native-only, and the highest-value thing you can add. Start here.

Crashlytics

@react-native-firebase/crashlytics
FreeDay one

Realtime crash, ANR and non-fatal reporting, with identical failures grouped into one issue ranked by how many users it hits.

In React Native it catches three different things: native crashes on either platform, unhandled JS exceptions (which reach it as fatals with the JS stack), and whatever you hand it via recordError. That third one is the workhorse — a caught API failure that you recovered from gracefully is invisible everywhere else, and it's usually the thing that's quietly ruining someone's session.

The grouping is what makes it better than Play Console vitals. A thousand reports collapse into one issue with a user count, an affected-version breakdown and a device distribution, so you can tell "everyone is broken" from "one OEM on Android 12 is broken."

TypeScript — make the reports useful
import {
  getCrashlytics, recordError, log, setUserId, setAttributes,
} from '@react-native-firebase/crashlytics';

const crashlytics = getCrashlytics();

setUserId(crashlytics, accountId);           // opaque id, never an email
await setAttributes(crashlytics, {
  locale: 'hi',
  session_mode: 'guest',
});

// Breadcrumb + non-fatal. The log lands in the report's timeline.
try {
  await api.fetchForecast(id);
} catch (e) {
  log(crashlytics, `forecast failed for ${id}`);
  recordError(crashlytics, e as Error);
  showRetry();
}

Record 5xx, not 4xx. A 400 is user input you already showed a message for; recording it just floods the dashboard until you stop looking at it. Same for plain offline failures. Record server faults, engine errors, and the catch blocks that silently return null.

Google Analytics

@react-native-firebase/analytics
Free, unlimited eventsUnlocks targeting

Event-based product analytics — and the audience engine that Remote Config, A/B Testing, In-App Messaging and FCM all target against.

Its second job matters more than its first. Analytics is what defines an "audience," and every engagement product consumes those audiences. Add it even if you never open the dashboard.

Two RN-specific notes. There's no automatic screen tracking, because Firebase has no idea what React Navigation is — you wire it yourself. And instrument at the store or data layer, on the outcome of an action rather than on button presses, so every screen that triggers the same action is covered once.

TypeScript — screen views from React Navigation
import { getAnalytics, logScreenView, logEvent, setUserProperty }
  from '@react-native-firebase/analytics';

const analytics = getAnalytics();
const navRef = useNavigationContainerRef();
const previous = useRef<string>();

<NavigationContainer
  ref={navRef}
  onReady={() => { previous.current = navRef.getCurrentRoute()?.name; }}
  onStateChange={async () => {
    const name = navRef.getCurrentRoute()?.name;
    if (name && name !== previous.current) {       // de-dupe, or you double-count
      previous.current = name;
      await logScreenView(analytics, { screen_name: name, screen_class: name });
    }
  }}
/>
TypeScript — custom events and user properties
// Fire on the outcome, from the store method that owns it.
await logEvent(analytics, 'profile_created', { is_self: true, mode: 'account' });

// Durable traits become audience dimensions. Max 25 per project.
await setUserProperty(analytics, 'chart_style', 'north');

// GA limits, silently enforced: name ≤ 40 chars, ≤ 25 params,
// values ≤ 100 chars. Over-limit events are dropped with no error.

Performance Monitoring

@react-native-firebase/perf
Free

App start time, screen rendering, and the duration and success rate of every network request — measured on real devices in the field.

Install it and app-start and HTTP traces arrive with no instrumentation. Then add custom traces around the flows you care about. The value is that your own phone tells you nothing: the question is what a three-year-old mid-range Android on a patchy connection experiences, and this is how you find out.

TypeScript — custom trace
import { getPerformance, trace } from '@react-native-firebase/perf';

const t = trace(getPerformance(), 'panchang_compute');
await t.start();
const result = await computePanchang(date);
t.putAttribute('source', cached ? 'cache' : 'engine');
t.putMetric('yoga_count', result.yogas.length);
await t.stop();

Messaging & config

Changing what the app does, and what it says, without shipping a build.

Cloud Messaging

@react-native-firebase/messaging
FreeThe only push option

Push notifications and silent data messages, delivered through the OS's own persistent connection.

There is no realistic alternative on mobile — every push provider you've heard of is a layer on top of FCM and APNs. Two message shapes behave very differently, and the difference catches everyone once:

  • A message with a notification payload is drawn by the OS when your app is backgrounded. Your JS never runs until the user taps it.
  • A data-only message always reaches your handler, so you control the notification completely — but delivery is subject to Doze and App Standby unless it's high priority.

The RN-specific rule is about where the background handler is registered. It must be at module scope in your entry file, before the app component registers — when a message arrives with your app killed, the OS spins up the JS runtime, runs that file, and expects the handler to already exist. Put it inside a component and it will work in the foreground and mysteriously not in the background.

index.ts — before anything else
import { getMessaging, setBackgroundMessageHandler }
  from '@react-native-firebase/messaging';

// Module scope. Not in a component, not in an effect.
setBackgroundMessageHandler(getMessaging(), async (message) => {
  await cachePanchang(message.data?.date);
});

import 'expo-router/entry';  // or registerRootComponent(App)
TypeScript — permission, token, foreground, topics
import {
  getMessaging, getToken, onMessage, onTokenRefresh, subscribeToTopic,
} from '@react-native-firebase/messaging';
import * as Notifications from 'expo-notifications';

const messaging = getMessaging();

// Ask at a moment where the value is obvious — not on first launch.
const { status } = await Notifications.requestPermissionsAsync();
if (status !== 'granted') return;

const token = await getToken(messaging);
await api.registerDevice(token);
onTokenRefresh(messaging, (t) => api.registerDevice(t));  // rotates on reinstall

// Topics beat per-device fan-out for broadcast content.
await subscribeToTopic(messaging, 'daily_panchang_hi');

// Foreground messages don't show a tray notification — you draw them.
const unsub = onMessage(messaging, async (m) => showInAppBanner(m.notification));

v26 deprecated RNFirebase's own permission helpers. Use a real notifications library — expo-notifications or Notifee — for permissions, channels and display, and let the messaging module do delivery only. On Android 13+ POST_NOTIFICATIONS is a runtime permission; on iOS you need an APNs key uploaded to Firebase before anything arrives at all.

Remote Config

@react-native-firebase/remote-config
FreeUnderrated

Server-controlled key–value pairs the app fetches at runtime, so you can change behaviour without shipping a build.

Feature flags, kill switches, tunable constants, seasonal copy, staged rollouts. You bake defaults into the bundle so the app works offline and on first launch, then override them from the console for everyone or a targeted slice — app version, country, language, Analytics audience, or a random percentage.

In an app store world the kill switch alone justifies it. A broken feature goes off for every user in minutes, instead of however long review plus rollout takes. It also pairs naturally with over-the-air JS updates: config decides whether a feature is on, OTA changes what the feature does.

TypeScript
import {
  getRemoteConfig, setDefaults, setConfigSettings,
  fetchAndActivate, getBoolean, getNumber, onConfigUpdated,
} from '@react-native-firebase/remote-config';

const config = getRemoteConfig();

await setDefaults(config, {
  match_feature_enabled: false,
  free_profile_limit: 3,
});

await setConfigSettings(config, {
  // Default throttle is ~12 hours. Set 0 in dev or you'll
  // swear your console changes aren't working.
  minimumFetchIntervalMillis: __DEV__ ? 0 : 3600_000,
});

await fetchAndActivate(config);

if (getBoolean(config, 'match_feature_enabled')) showMatchTab();

// React to console changes mid-session
onConfigUpdated(config, () => fetchAndActivate(config));

A/B Testing & In-App Messaging

console · @react-native-firebase/in-app-messaging
Free

Controlled experiments on Remote Config values, and console-composed cards and banners for users who are already inside the app.

A/B Testing isn't a separate SDK at all — it's a layer over Remote Config, so if you have that, you have this. Define variants in the console, pick a goal metric, and it handles randomised assignment and the statistics.

In-App Messaging is the counterpart to push: push is for absent users, this is for present ones. Targeting reuses Analytics audiences and events, so "users who opened the match screen three times but never created one" is a trigger you express in the console without shipping code.

Identity & abuse

Who the user is, and whether the caller is really your app.

Authentication

@react-native-firebase/auth
Free to 50k MAU

Managed sign-in — email/password, Google, Apple, phone, anonymous — producing one stable UID and a signed ID token.

Be clear about whether you need this. If you already run your own backend with your own accounts, Firebase Auth is not automatically an upgrade; it's a second identity system to reconcile. But the moment you want Firestore or Cloud Storage, you need it, because their security rules are written against request.auth.uid and nothing else. The bridge between the two worlds is a custom token: your server mints one with the Admin SDK, the app signs in with it, and your existing account id becomes the Firebase UID.

The anonymous provider is the underrated one — call it on first launch and the user has a real UID, real per-user data and real security rules before they've seen a login screen. Link a real provider later and everything carries over on the same UID.

TypeScript
import {
  getAuth, signInAnonymously, signInWithCustomToken,
  onAuthStateChanged, linkWithCredential, GoogleAuthProvider,
} from '@react-native-firebase/auth';

const auth = getAuth();

// Zero-friction start
if (!auth.currentUser) await signInAnonymously(auth);

// Or bridge your own backend's accounts
const { firebaseToken } = await api.login(email, password);
await signInWithCustomToken(auth, firebaseToken);

// Session state, as a hook
useEffect(() => onAuthStateChanged(auth, setUser), []);

Custom claims are how you do roles. Set them server-side with the Admin SDK and they ride inside the ID token, where rules read them as request.auth.token.premium. A role stored in a user-writable Firestore field is not a permission — it's a suggestion.

App Check

@react-native-firebase/app-check
FreeBefore launch

Attests that a request came from your genuine, unmodified app on a real device, and lets Firebase reject everything else.

Auth answers "which user is this?" App Check answers "is this even my app?" Without it, anyone can pull the config out of your APK and hit your Firestore or your AI endpoint from a script — running up your bill and scraping whatever your rules allow signed-in users to see. Android uses Play Integrity, iOS uses App Attest.

Turn it on in monitoring mode first, watch the console until legitimate traffic looks clean, then enforce. You register a debug token for your emulator or CI, or that traffic gets blocked too — budget one confused afternoon for this.

Phone Number Verification

bundled in @react-native-firebase/auth
GA 2026MeteredAndroid only

Gets the device's carrier-verified phone number with user consent — no SMS, no OTP screen, no typing.

Distinct from Auth's phone sign-in, which sends a code the user reads and retypes. This goes to the carrier and returns the number plus a verification token in one call: no delivery failures, no SMS-pumping fraud, no six-digit input. It's aimed at markets where phone-number onboarding is standard and SMS OTP is the biggest drop-off point in the funnel. Keep SMS as a fallback — it won't work on every carrier or on Wi-Fi-only devices.

Data & storage

Optional if you already have a backend — but this is the half of Firebase that costs money, so read the billing note.

Cloud Firestore

@react-native-firebase/firestore
MeteredDefault choice

A NoSQL document database that syncs to the device, works offline, and pushes live updates to every listening client.

Data is documents — JSON-ish maps — inside collections, and collections nest under documents. Queries are shallow and indexed: you filter and order, but you cannot join, and every query must be satisfiable by an index. That constraint is what makes cost predictable, since a query's price tracks the size of the result rather than the collection.

The feature you build around is the snapshot listener. Attach one and you get the cached data immediately, then every subsequent change streamed. Writes hit the local cache first and sync when the network returns, so your UI updates instantly whether or not there's signal. In React Native this maps cleanly onto useEffect — and the cleanup function is not optional.

Use it when
You want realtime sync across devices, offline support for free, and your data suits denormalised documents.
Skip it when
You already have a REST backend that owns this data. Two sources of truth is a worse problem than an extra network hop.
TypeScript — listener as a hook
import {
  getFirestore, collection, query, where, orderBy, onSnapshot, doc, setDoc,
} from '@react-native-firebase/firestore';

const db = getFirestore();

function useJournal(uid: string) {
  const [entries, setEntries] = useState<Entry[]>([]);

  useEffect(() => {
    const q = query(
      collection(db, 'journal'),
      where('ownerUid', '==', uid),
      orderBy('createdAt', 'desc'),
    );
    // Returning the unsubscribe is what stops the leak — and the bill.
    return onSnapshot(q, (snap) =>
      setEntries(snap.docs.map((d) => ({ id: d.id, ...d.data() }) as Entry)),
    );
  }, [uid]);

  return entries;
}

await setDoc(doc(db, 'journal', id), { ownerUid: uid, nature: 'good' });

Access control is a separate rules file you deploy alongside the data. Write it the same day you write the first query, not later:

firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{db}/documents {
    match /journal/{entryId} {
      allow read, write: if request.auth != null
        && request.auth.uid == resource.data.ownerUid;
    }
  }
}

Realtime Database

@react-native-firebase/database
Metered

Firebase's original database: one big JSON tree, synced with very low latency.

Not deprecated, just older. It bills on bandwidth and stored gigabytes rather than per document read, which makes it cheaper when tiny payloads change many times a second — presence, typing indicators, live positions. The trade is a much weaker query model: one child key at a time, no compound queries. For a normal app, Firestore is the better default.

Cloud Storage

@react-native-firebase/storage
Metered

File storage for user-generated content, with resumable uploads and the same style of security rules as Firestore.

Databases hold structured fields; Storage holds bytes. The native SDK handles pausing and resuming across network drops, which matters far more on mobile than on the web. Standard pattern: upload the file, then write the resulting path — not the download URL, which can expire — into a Firestore document with your metadata.

TypeScript — upload with progress
import { getStorage, ref, putFile, getDownloadURL } from '@react-native-firebase/storage';

const r = ref(getStorage(), `avatars/${uid}.jpg`);
const task = putFile(r, localUri);        // a local file path, not a blob

task.on('state_changed', (s) =>
  setProgress(s.bytesTransferred / s.totalBytes));

await task;
const url = await getDownloadURL(r);

SQL Connect

no native module — JS SDK only
Renamed 2026Not in RNFirebase

A managed PostgreSQL database reached through a generated, type-safe SDK — Firebase's answer to "but my data really is relational."

Launched as Data Connect, renamed SQL Connect in 2026. You declare the schema and the allowed operations in GraphQL; Firebase provisions the Postgres tables and generates a typed client containing exactly those operations, so the app can't issue arbitrary queries. Genuinely good — but React Native Firebase has no module for it, so from RN you'd reach it through the firebase JS SDK or behind a Cloud Function. If you want relational data and a native module, that's an argument for keeping your own backend.

Server-side logic

For work that must not happen on a phone.

Cloud Functions

@react-native-firebase/functions
Metered · needs Blaze

TypeScript or Python that Google runs for you, triggered by a call from your app or automatically by a Firebase event.

Sooner or later something must not live in the bundle: a payment, an API key with real money behind it, a push sent to another user, a field users shouldn't be able to write. That's a function. Two kinds matter here — callable functions you invoke from the app, which get the user's auth and App Check tokens attached automatically, and background triggers that fire on a Firestore write, a new user, a Storage upload or a schedule.

That last one is how you send scheduled push without owning a server: a scheduled function runs daily, queries who wants a notification, and calls FCM through the Admin SDK.

TypeScript — both sides
// App
import { getFunctions, httpsCallable } from '@react-native-firebase/functions';

const createOrder = httpsCallable(getFunctions(), 'createOrder');
const { data } = await createOrder({ cartId });

// ── functions/src/index.ts ──────────────────────────────
export const createOrder = onCall({ enforceAppCheck: true }, async (req) => {
  if (!req.auth) throw new HttpsError('unauthenticated', 'Sign in');
  return { orderId: await charge(req.auth.uid, req.data.cartId) };
});

// Scheduled push, no server of yours involved
export const dailyPanchang = onSchedule('0 6 * * *', async () => {
  await getMessaging().send({
    topic: 'daily_panchang_hi',
    notification: { title: todayTithi(), body: todaySummary() },
  });
});

Extensions are pre-packaged functions you install and configure instead of writing — image resizing on upload, Firestore-to-BigQuery sync, transactional email, Stripe subscriptions. Worth checking the catalogue before writing anything generic.

AI

One product, and it replaced three older ones.

Firebase AI Logic

@react-native-firebase/ai
Actively evolvingMetered

Call Gemini models from the app without your API key ever being in the bundle, and without running a proxy server.

The naive approach — a Gemini key in your JS — is the same as publishing it, and a JS bundle is even easier to read than a compiled binary. AI Logic routes through a Firebase-hosted endpoint that holds the credential, enforces App Check and applies your limits. You pick a backend (the Gemini Developer API for the simple path, Vertex AI for enterprise controls) and a model.

The 2026 release added grounding with Google Search and Maps, function calling, and a template-only mode that keeps prompts server-side so they can't be extracted or overridden from the client.

TypeScript
import { getApp } from '@react-native-firebase/app';
import { getAI, getGenerativeModel, GoogleAIBackend } from '@react-native-firebase/ai';

const ai = getAI(getApp(), {
  backend: new GoogleAIBackend(),
  appCheck: appCheckInstance,          // do not ship without this
});

const model = getGenerativeModel(ai, { model: 'gemini-3.8-flash' });

const result = await model.generateContent(
  `Explain this chart in simple Hindi: ${JSON.stringify(chart)}`,
);
setAnswer(result.response.text());

An unprotected AI endpoint is a stranger's free Gemini quota, billed to you. App Check is not optional here in a way it arguably is elsewhere — this is the one product where an abuser's costs land directly on your card.

Release & testing

Two console products, no app code.

App Distribution ships pre-release builds straight to named testers, outside Play's review and track system — an email, a small tester app, and each build lands on their phone. It plugs into EAS and CI, so a merge can be on your QA team's devices in minutes. Note that Expo's own internal distribution covers much of the same ground, so this is worth adding mainly if you want Firebase's tester management and in-app update prompts.

Test Lab runs your app on real physical devices in Google's data centre. Robo test needs no test code at all — it crawls your UI and reports crashes with video — while instrumentation mode runs your suite across a device matrix in parallel. For RN this is the cheapest way to find the crash that only happens on one OEM's skin without buying that phone. If you're already running Maestro flows, those are the natural thing to point at it.

Full module matrix

Every package in the org at v26.4.0, and what it's for. Bookmark this bit.

PackageProductBills on
appCore — required by every other module
analyticsGoogle Analytics / GA4Free
crashlyticsCrash & non-fatal reportingFree
perfPerformance MonitoringFree
messagingCloud Messaging (push)Free
remote-configRemote Config & A/B TestingFree
in-app-messagingIn-app cards and bannersFree
app-checkApp attestationFree
installationsPer-install IDs (used by the above)Free
app-distributionTester buildsFree
authAuthentication + phone verification50k MAU free
firestoreCloud FirestoreReads / writes / storage
databaseRealtime DatabaseBandwidth / storage
storageCloud Storage for filesStorage / downloads
functionsCallable Cloud FunctionsInvocations / compute
aiFirebase AI Logic (Gemini)Model tokens
vertexaiLegacy wrapper — use aiModel tokens
mlFirebase ML — shutting down Jun 2027

Two things Firebase has that RNFirebase doesn't: SQL Connect (JS SDK only) and Hosting / App Hosting (web deployment — though you may still want Hosting for your privacy policy URL, which Play requires, and for the assetlinks.json file behind Android App Links).

What it costs

Two plans. The trap isn't the price — it's which axis you're billed on.

Spark is free with hard caps: no payment method, and service stops when you hit a limit. Blaze is pay-as-you-go — same free allowances, then usage above them — and it's required for Cloud Functions, for any outbound network call from a function, and for most newer products. New Blaze accounts typically get $300 of Google Cloud credit.

The good news for a mobile app: the entire telemetry and engagement half is free at any scale. Analytics, Crashlytics, Performance, Cloud Messaging, Remote Config, A/B Testing, In-App Messaging, App Distribution and App Check never bill on usage. You can run a large app on Spark indefinitely if you don't touch the data products.

ProductBilled onFree allowance
Cloud FirestoreDocument reads, writes, deletes · stored GiB50k reads, 20k writes, 20k deletes per day; 1 GiB
Realtime DatabaseBandwidth and stored GB1 GB stored, 10 GB/month down
Cloud StorageStored GB, downloads, operations5 GB stored; 100 GB/month download
Cloud FunctionsInvocations, compute time, egress2M invocations/month (Blaze only)
AuthenticationMonthly active users50k MAU
AI LogicGemini tokens in and outDepends on backend and model
Test LabDevice-minutesDaily quota of device time

Firestore's read counter is where surprise bills come from. Every document a query returns is a billed read, every time. A listener recreated on each render, or a useEffect missing its dependency array, re-subscribes constantly and burns reads at a rate that looks fine with one tester and ruinous at ten thousand users. Paginate with limit(), keep listeners in a store rather than a component, and set a budget alert in Cloud Billing on day one.

Gone and going

Most RN Firebase tutorials online reference at least one of these. Check the date on anything you read.

WhatStatusInstead
Namespaced API — firebase.auth(), analytics()Deprecated in v22, removed in v26Modular: import the function, pass the instance first
Dynamic LinksShut down 25 Aug 2025; package frozen at 22.4.0Expo Linking / App Links; a third-party attribution SDK if you need deferred deep links
messaging().requestPermission()Deprecated in v26expo-notifications or Notifee for permissions and display
Old bridge / non-TurboModule setupReplaced in v26New Architecture, on by default in Expo SDK 52+
Firebase MLDeprecated; shutdown 15 Jun 2027ML Kit for on-device vision; AI Logic for generative
@react-native-firebase/vertexaiSuperseded@react-native-firebase/ai
Data ConnectRenamed 2026SQL Connect — still JS SDK only

RN-specific traps

The failures that are about React Native, not about Firebase.

  • Testing in Expo Go. Every native Firebase call silently no-ops. You need a dev build. This accounts for most "my events don't show up" threads.
  • Forgetting Analytics DebugView. Events batch for up to an hour by default, and the standard Events report lags about 24 hours — never use it to verify a new event. Turn on debug mode (adb shell setprop debug.firebase.analytics.app <package>) and watch DebugView, which is near-instant.
  • Crashlytics silent in debug. Without crashlytics_debug_enabled in firebase.json, recordError does nothing locally and you'll conclude it's broken. Also: non-fatals upload on the next launch, so kill and reopen the app before checking.
  • Background message handler inside a component. It must be at module scope in your entry file. Foreground will work, background won't, and the difference is easy to miss in testing.
  • Unmocked native modules in Jest. The native modules don't exist in the test environment, so any suite that transitively imports your telemetry module explodes. Mock the Firebase packages globally in your Jest setup file — another reason for the single wrapper module.
  • Listeners without cleanup. onSnapshot, onMessage and onAuthStateChanged all return an unsubscribe function. Return it from useEffect. Firestore listeners that leak also bill.
  • Mixing the two SDKs for one product. Installing both firebase and @react-native-firebase/firestore and using both gives you two independent clients, two caches, and desync bugs that look like ghosts.
  • One Firebase project for dev and prod. Use separate projects with separate config files per build profile. Test data mixed into production analytics is very hard to undo.
  • Skipping the Local Emulator Suite. firebase emulators:start runs Firestore, Auth, Functions and Storage locally — free, instant, and the only sane way to iterate on security rules. Point the app at it with connectFirestoreEmulator in dev.
  • iOS without useFrameworks: "static". RNFirebase requires it. The pod install error it produces otherwise doesn't mention Firebase at all.

A worked example

Jyotisha, a Hindu calendar app: Expo 54, RN 0.81.5, RNFirebase 26.4.0, Android-only, Spark plan.

Abstract advice is easy to agree with and hard to act on, so here is a real app at the two-product stage — Analytics and Crashlytics wired, nothing else. It gets the things right that usually go wrong: latest RNFirebase, modular API throughout, every Firebase import isolated in one telemetry.ts, both packages mocked globally in Jest, and crashlytics_debug_enabled set so local testing actually reports. Events sit at the store layer on outcomes rather than on button presses — the right call, and the one most teams get wrong. Nothing in the traps list above applies to it.

So the interesting question is what comes next. On the free plan, in rough order of value per unit of effort:

  1. 01Remote ConfigFree, no new native surface beyond the package, and it gives you a kill switch plus flags for shipping half-finished features dark. The one to add first — it changes how you release, not just what you measure.
  2. 02Cloud MessagingDaily panchang and tithi notifications are the obvious fit, and topics (daily_panchang_hi, daily_panchang_en) mean you don't need to store device tokens at all. Sending on a schedule needs a Cloud Function, which needs Blaze — or your existing backend can call the FCM HTTP API instead and keep you on Spark.
  3. 03User propertiesMissing today. chart_style, locale and session_mode turn every existing report into a segmented one for about ten lines of code — the cheapest item on this list.
  4. 04Performance MonitoringFree, and it would show what the forecast run and the astrology engine calls actually cost on a mid-range phone in India, rather than on a developer's emulator.
  5. 05App CheckWorth it once anything on the backend costs money per call. Lower priority while Firebase is only receiving telemetry.

Firestore, Storage and Auth are the ones to leave alone here. The app already has a backend that owns accounts and profile data, and adding Firebase's versions would buy two sources of truth and a reconciliation problem — a worse trade than the offline sync is worth. That calculus is the general lesson: adopt the free telemetry and engagement half eagerly, and the data half only when you actually need what it does.