Firebase: push, Remote Config & Crashlytics
Reach users with notifications, change app behaviour without a release, and know when it crashes.
In 30 seconds
Push notifications travel from your server to Google's or Apple's push service, which delivers them to a specific device token. Firebase Cloud Messaging (FCM) gives you one API for both platforms. Firebase also offers Remote Config, to switch features or force an update without shipping a new build, and Crashlytics and Analytics, to see crashes and usage in production.
Why I use it
- I use Firebase because one service covers the operational needs of almost every app I ship: push, remote switches, crash reports and analytics.
- Push is a core business feature in ordering and loyalty apps — order status, rewards, campaigns — so I treat delivery, permission timing and tap handling as product work, not an afterthought.
- Remote Config gives me a safety valve: a force-update or maintenance screen can be switched on in minutes when a backend change or a bug requires it.
- Crashlytics tells me about problems before users write reviews, with stack traces tied to the exact build.
How it works
01How push notifications actually work
Your server never talks to a phone directly. On iOS the operating system keeps one connection to APNs (Apple Push Notification service); on Android, Google Play services keeps one connection to FCM. When your app starts, it asks the OS for a device token — an address for this app on this device — and sends it to your backend, linked to the user. To notify the user, your backend calls the FCM HTTP v1 API with that token; FCM delivers directly on Android and forwards to APNs for iOS (you upload an APNs auth key to Firebase once). Tokens change: after reinstall, restore or periodically, so listen for token refresh and update the backend. When FCM reports a token as unregistered, delete it. One user can have several devices, so store many tokens per user.
- Device token: address of one app install, not of a person
- APNs auth key (.p8) uploaded to Firebase for iOS delivery
- FCM HTTP v1 API with a service account; the legacy API is gone
- Topics: send one message to many subscribers (e.g. 'promo-hy')
02Permissions and getting the token
iOS always needs user permission to show notifications, and Android needs the POST_NOTIFICATIONS runtime permission from Android 13 (API 33). If a user taps 'Don't allow', you usually cannot ask again — they must go to Settings. So ask at a moment the value is obvious: after the first order ('Get notified when your coffee is ready?'), not on the first screen. Show a short in-app explanation before the system dialog. iOS also supports provisional authorization, which delivers quietly to the notification centre without a prompt. With React Native Firebase the flow is: request permission, get the token, send it to your API with the platform and app version, and subscribe to token refresh.
import { getMessaging, requestPermission, getToken, onTokenRefresh,
AuthorizationStatus } from "@react-native-firebase/messaging";
import { PermissionsAndroid, Platform } from "react-native";
export async function registerForPush(api: Api) {
const messaging = getMessaging();
if (Platform.OS === "android" && Platform.Version >= 33) {
await PermissionsAndroid.request("android.permission.POST_NOTIFICATIONS");
}
const status = await requestPermission(messaging);
if (status === AuthorizationStatus.DENIED) return;
const token = await getToken(messaging);
await api.post("/devices", { token, platform: Platform.OS });
onTokenRefresh(messaging, (t) => api.post("/devices", { token: t, platform: Platform.OS }));
}
03Notification vs data messages, foreground and taps
An FCM message can carry a notification block (title, body), a data block (your own key-value strings), or both. Notification messages are displayed by the OS automatically when the app is in the background or killed. Data-only messages are delivered to your code, which decides what to do; on iOS they are background pushes that the system may throttle or delay, so never rely on them for user-visible alerts. When the app is in the foreground, the OS does not show the banner by default: you receive it in onMessage and decide — show an in-app toast, update a badge, or display a local notification with Notifee. Handle taps in three states: foreground, background (onNotificationOpenedApp) and cold start (getInitialNotification). Put a deep-link route in data and navigate with it.
{
"message": {
"token": "DEVICE_TOKEN",
"notification": { "title": "Order ready", "body": "Your latte is waiting at the counter" },
"data": { "type": "order_status", "orderId": "1042", "link": "app://orders/1042" },
"android": { "priority": "high", "notification": { "channel_id": "orders" } },
"apns": { "payload": { "aps": { "sound": "default" } } }
}
}
04Sending from the backend reliably
Send pushes from a queue worker, not inside the HTTP request: an order-status change writes a job, and the worker calls FCM with retries and backoff. Use the Firebase Admin SDK or the HTTP v1 endpoint with a service-account credential stored as a secret on the server — never in the app. Save every campaign or message in your own database too, so the app can show an in-app inbox; pushes are not guaranteed to arrive and users clear them. Respect quiet hours and user preferences (order updates on, marketing off), and on Android create separate notification channels so users can mute promotions without losing order updates. Clean up tokens that FCM returns as unregistered. For mass campaigns, use topics or batched sends and schedule them through your own job system.
05Remote Config: force update, maintenance, feature flags
Remote Config is a set of key-value parameters stored in Firebase that the app fetches at runtime. You ship defaults inside the app, fetch and activate on launch with a sensible minimum fetch interval, and read values as booleans, numbers or strings. Conditions let you target by platform, app version, country or percentage of users. Three uses pay for themselves. Force update: store min_supported_version per platform; if the installed version is lower, show a blocking screen with a store link — essential when an old app would break against a new API. Maintenance mode: a flag that shows a friendly screen during migrations. Feature flags and gradual rollout: enable a new checkout for 10% of users, watch Crashlytics, then go to 100%. Keep logic safe if fetch fails: the app must work on defaults.
import { getRemoteConfig, setDefaults, setConfigSettings,
fetchAndActivate, getValue } from "@react-native-firebase/remote-config";
import { Platform } from "react-native";
import DeviceInfo from "react-native-device-info";
export async function checkAppGate() {
const rc = getRemoteConfig();
await setDefaults(rc, { min_version_ios: "1.0.0", min_version_android: "1.0.0", maintenance: false });
await setConfigSettings(rc, { minimumFetchIntervalMillis: 15 * 60 * 1000 });
await fetchAndActivate(rc).catch(() => undefined); // offline: use cached/defaults
const key = Platform.OS === "ios" ? "min_version_ios" : "min_version_android";
if (getValue(rc, "maintenance").asBoolean()) return "maintenance";
if (isLower(DeviceInfo.getVersion(), getValue(rc, key).asString())) return "force_update";
return "ok";
}
06Crashlytics and Analytics
Crashlytics collects native crashes and, with React Native Firebase, JavaScript errors you record, grouped by issue and tied to app version and device. Make sure symbol files (dSYMs on iOS, mapping files on Android) are uploaded during the build, or stack traces stay unreadable. Add context: set a non-personal user ID, log breadcrumbs before risky steps (payment start, payment return), and record handled errors from API failures that matter. Watch the crash-free users rate per release; during a staged rollout it is your signal to pause. Analytics records events such as screen_view, add_to_cart, begin_checkout and purchase. Define a small event plan with consistent names before coding, avoid sending personal data, and respect consent: disable collection until the user agrees where the law requires it.
Use it when
- Any app that needs transactional notifications: order status, bookings, reminders, security alerts.
- You need remote switches — force update, maintenance mode, gradual feature rollout — without a store release.
- You want crash reporting and product analytics with very little setup on both platforms.
Skip it when
- Users are mostly on devices without Google Play services (some markets) — you need an alternative Android push provider.
- Strict data-residency or privacy rules forbid sending device or usage data to Google — consider self-hosted analytics and a provider that fits.
- Real-time in-app updates while the app is open — use WebSockets or SSE; push is not a live data channel.
Alternatives
- Expo Notifications
Unified push API and service for Expo apps; uses FCM and APNs underneath.
- OneSignal / Braze
Hosted engagement platforms with segmentation, campaigns and analytics on top of push.
- Direct APNs + FCM
Call Apple and Google directly from your backend for full control and no extra vendor.
- Sentry
Error and performance monitoring alternative to Crashlytics, with strong JS and source-map support.
Best practices
- Ask for notification permission at a meaningful moment, with a short explanation first.
- Store multiple tokens per user, refresh them, and delete tokens FCM reports as unregistered.
- Send from a queue with retries; keep an in-app inbox because pushes are not guaranteed.
- Use Android notification channels and user preferences to separate transactional and marketing messages.
- Ship Remote Config defaults so the app works offline; use a force-update and maintenance switch from day one.
- Upload dSYMs and mapping files on every build and watch crash-free users per release.
- Keep service-account keys on the server only and never put personal data in analytics events.
Common mistakes
- Requesting push permission on the very first screen and losing most users' consent permanently.
- Relying on data-only messages on iOS for important alerts, which the system may delay or drop.
- Ignoring foreground messages, so users with the app open never see the notification.
- Not handling the cold-start tap, so tapping a notification opens the home screen instead of the order.
- Shipping without a force-update switch, then being unable to stop old builds from calling a changed API.
Deep dive
07Delivery internals: priority, TTL, collapsing and Doze
Delivery is shaped by power management. Android's Doze mode batches network access for idle phones; normal-priority FCM messages wait for a maintenance window, while high-priority messages can wake the device. Google may deprioritise apps whose high-priority messages do not lead to a visible notification, so use high priority only for alerts the user sees. On iOS, apns-priority 10 means deliver now and 5 means deliver when convenient; background (content-available) pushes must use priority 5 and are throttled by the system. Every message has a lifetime: android.ttl and the apns-expiration header decide how long the service keeps trying an offline device. A collapse key (Android) or apns-collapse-id (iOS) replaces older undelivered messages with the newest one, so a phone that comes back online shows one 'cart updated' message, not ten. Keep payloads small: FCM rejects messages above about 4 KB.
{
"message": {
"token": "DEVICE_TOKEN",
"data": { "type": "cart_sync", "cartId": "c_981" },
"android": { "priority": "normal", "ttl": "3600s", "collapse_key": "cart" },
"apns": {
"headers": {
"apns-priority": "5",
"apns-push-type": "background",
"apns-collapse-id": "cart"
},
"payload": { "aps": { "content-available": 1 } }
}
}
}
08Sending at scale: batching, errors and quotas
The Firebase Admin SDK sends a list of up to 500 tokens in one call with sendEachForMulticast, returning one result per token in the same order. Classify every failure. registration-token-not-registered means the app was uninstalled or the token rotated: delete it. invalid-argument means your payload is wrong: fix the code, do not retry. Quota-exceeded (HTTP 429) and unavailable or internal errors (503, 500) are temporary: retry with exponential backoff and respect a Retry-After header. Project-level quotas exist, so large campaigns should go through a queue that paces sends instead of a loop that fires everything at once. Pacing also protects your own backend: if 200,000 people receive a promotion in the same second, many will open the app in the same minute.
import { getMessaging } from "firebase-admin/messaging";
export async function sendToUser(tokens: string[], title: string, body: string, link: string) {
const res = await getMessaging().sendEachForMulticast({
tokens, // up to 500 per call
notification: { title, body },
data: { link },
});
const dead = res.responses
.map((r, i) => (r.error?.code === "messaging/registration-token-not-registered" ? tokens[i] : null))
.filter((t): t is string => t !== null);
await removeTokens(dead); // keep the token table clean
return { sent: res.successCount, failed: res.failureCount };
}
09Privacy and security of notifications
A notification is shown on a lock screen, mirrored to watches and sometimes read aloud, so treat its text as public. Never put one-time codes, balances, health details or full addresses in the title or body; say 'Your order has an update' and show details inside the app after authentication. On iOS a Notification Service Extension can decrypt an encrypted payload or download rich media before display (mutable-content), which lets you send less sensitive data through Apple's and Google's servers. The service-account credential that sends pushes is powerful: give it only the Firebase Cloud Messaging roles it needs, keep it in a secret store and rotate it. Device tokens are not secrets, but they are personal data linked to a user: unlink them on sign-out and delete them when an account is deleted.
10Remote Config internals and pitfalls
Remote Config is client-side configuration, not a security boundary: values are downloaded to the device and can be read or altered there, so never store secrets in it or use it to grant permissions — the server must enforce access. The SDK caches values and throttles fetches; the default minimum fetch interval is 12 hours, so lower it deliberately and avoid fetching on every screen. Decide how values activate: fetchAndActivate at launch is simple but can change the UI after it appears; fetching in the background and activating on the next launch avoids flicker. Real-time Remote Config can push changes to running apps when you need fast kill-switches. Percentage conditions are stable per app installation, so the same user stays in the same group. Every publish creates a template version you can roll back, which is useful when a bad value breaks something.
Real-world scenarios
iOS pushes stopped after going to production
Problem
Android receives notifications, and iOS worked in development builds, but TestFlight and App Store users get nothing. The backend logs show FCM accepting the requests.
How to solve it
Walk the iOS chain step by step. Confirm an APNs authentication key (.p8) is uploaded in the Firebase project settings with the correct Key ID and Team ID, and that the Firebase iOS app's bundle ID matches the shipped app exactly. Check the Push Notifications capability is enabled so the production entitlement is in the signed build. Confirm the app requests permission and registers for remote notifications before reading the FCM token, and that the token stored on the backend came from the production build, not an old debug install. Finally send a test message from the Firebase console to one fresh token to isolate backend problems from configuration problems.
A campaign push took the backend down
Problem
Marketing sent a discount push to every user at 18:00. Within two minutes tens of thousands opened the app, the API's response times exploded and many users saw errors instead of the offer.
How to solve it
Pace the send. Campaigns go into a queue that sends in batches — for example several thousand devices per minute — or by time zone, instead of one burst. Make the landing screen cheap: the offer content is cached in Redis or a CDN, and the app shows cached content while fresh data loads. Load-test the campaign path before big sends. Coordinate with the backend: autoscaling or extra capacity warmed up before a planned campaign, and rate limits that degrade gracefully. Finally store each campaign in your database with its status, so a paused or partially sent campaign can resume without sending duplicates.
Users receive the same notification twice
Problem
Some users get two identical order-ready notifications. It affects people who reinstalled the app or own two devices, and sometimes happens after backend deploys.
How to solve it
Find which duplicate it is. Reinstalls create a new token while the old one may still be valid for a while, so store tokens per installation ID and replace the old row when a device registers again. Deploys and retries cause the second type: a queue job that times out after FCM accepted the message is retried and sends again. Give each notification a stable ID, record it as sent before acknowledging the job, and skip if it already exists. Use a collapse key or apns-collapse-id equal to that ID, so even if two copies reach the device, the newer replaces the older instead of stacking.
Cheat sheet
- FCM HTTP v1 send endpoint
POST https://fcm.googleapis.com/v1/projects/PROJECT_ID/messages:send- OAuth scope for sending
https://www.googleapis.com/auth/firebase.messaging- Android 13+ manifest permission
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />- Batch send to one user's devices
getMessaging().sendEachForMulticast({ tokens, notification, data }) // max 500- Subscribe devices to a topic
getMessaging().subscribeToTopic(tokens, "promo-hy")- Error that means delete the token
messaging/registration-token-not-registered- Replace older undelivered messages
android.collapse_key / apns-collapse-id header- Limit how long FCM retries
"android": { "ttl": "3600s" } / apns-expiration header- Show iOS banner in foreground (native)
completionHandler([.banner, .list, .sound])- Remote Config fetch interval in dev
setConfigSettings(rc, { minimumFetchIntervalMillis: 0 }) // never in production
In my real projects
- My restaurant, pet-shop, gym and coffee apps use Firebase Messaging, Analytics, Crashlytics and Remote Config, most of them with Notifee for foreground notifications.
- Remote Config drives force update and maintenance mode in my apps; the backends send FCM push campaigns and keep an in-app notification inbox.
- The gym CRM sends scheduled push campaigns, and the restaurant and gym admin panels use web push for staff alerts.
- For a game LiveOps console I built a Remote Config editor with history and diffs, plus FCM push to players; the POS SaaS sends FCM and APNs push, including iOS Live Activities.
Questions & answers
Q1What is the difference between notification and data messages in FCM?
A notification message has a title and body that the operating system shows automatically when the app is in the background or closed; your code is not needed to display it. A data message carries only your key-value payload and is delivered to the app, which decides what to do. On Android data messages are reliable with high priority; on iOS they become silent background pushes that the system may throttle. In practice I send both blocks: notification for the visible alert and data with the type, IDs and deep-link route for handling the tap.
Q2Why don't users see my notification when the app is open?
Because in the foreground the operating system hands the message to the app instead of showing a banner — the assumption is that the app will present it in context. With React Native Firebase you receive it in the onMessage listener. From there you decide: update the order screen if it is visible, show an in-app toast, or display a local notification with Notifee using the right Android channel. On iOS you can also configure presentation options to show banners in the foreground. Handling this explicitly is part of designing push, not a bug fix.
Q3How do you implement a force update in a mobile app?
I store a minimum supported version per platform in Remote Config, with a safe default inside the app. On launch the app fetches and activates config, compares its own version with the minimum using semantic version comparison, and if it is lower shows a blocking screen with a button to the store page. A softer 'recommended version' key can show a dismissible prompt. The backend can also return a specific error for unsupported clients as a second line of defence. This lets me change APIs safely without old builds breaking silently.
Q4Push token management — what can go wrong?
Tokens identify an app install, not a user, and they change after reinstall, data restore or rotation. If you store only one token per user, a second device stops getting messages. If you never delete stale tokens, you keep sending to dead addresses and skew delivery metrics. I save tokens per device with platform, app version and last seen date, update on refresh and sign-in, remove the token on sign-out so the next user of the phone does not receive private messages, and delete tokens FCM reports as unregistered.
Q5How would you roll out a risky feature safely?
Put it behind a Remote Config flag that defaults to off. Ship the build with the feature hidden, then enable it for internal testers using a condition, then for a small percentage of real users. Watch Crashlytics crash-free rate, error logs and key Analytics events such as checkout completion compared with the control group. If something goes wrong, turn the flag off in minutes without a new release. Once stable at 100%, remove the flag and the old code path so flags do not pile up as technical debt.
Senior-level questions
S1Design a notification system for an app with a million users.
I separate intent from delivery. Business events — order ready, booking reminder, campaign — create notification records in the database with type, user, payload, priority and a stable ID. A preference service decides per user and channel whether to send (transactional on, marketing opt-in, quiet hours, frequency caps). Accepted notifications go into queues split by priority, so a big campaign never delays an order-ready alert. Workers fan out to the user's active tokens through FCM in batches, classify errors, delete dead tokens and retry transient failures with backoff. Every record also appears in the in-app inbox, because push is best-effort. Metrics cover queue lag, send success, opens and opt-out rates. Trade-off: this is more moving parts than calling FCM directly, but it gives fairness, auditability and safe retries.
S2Can you guarantee a push notification is delivered exactly once?
No. The path crosses your queue, FCM, APNs or the Android transport, and a device that may be offline, in Doze or uninstalled; none of those offers end-to-end guarantees to you. What you can build is at-least-once sending with deduplication. On the server, each notification has a stable ID; the worker records success idempotently so retries after a timeout do not resend. On the device, a collapse ID or the app's own check of the notification ID hides duplicates. For anything that truly matters — a security alert, an order status — the source of truth is in-app state and the inbox, plus a fallback channel such as SMS or email when the business needs it. Push is a nudge to open the app, not the record.
S3Firebase Remote Config or a dedicated feature-flag service?
Remote Config is excellent for mobile-only switches: force update, maintenance mode, simple percentage rollouts and A/B tests tied to Firebase Analytics, with almost no infrastructure. Its limits appear when flags must be evaluated consistently across mobile, web and backend for the same user, when you need targeting by your own business attributes, approval workflows, audit trails beyond template versions, or server-side evaluation of sensitive rules. Then a dedicated flag service, open source or commercial, or a small internal flags table served by your API, fits better. A common hybrid: Remote Config for app-level gates that must work before login or when your API is down, and backend flags for business features, so the server stays the authority on what a user may do.
Glossary
- APNs
- Apple Push Notification service: Apple's servers that deliver all remote notifications to iPhones, iPads and Macs.
- FCM
- Firebase Cloud Messaging: Google's service that delivers to Android directly and to iOS through APNs.
- Device token
- An address for one app installation on one device, used by the push service to deliver messages.
- Topic
- A named group devices subscribe to, so one message reaches all subscribers without listing tokens.
- Collapse key
- An identifier telling the push service that a newer message replaces older undelivered ones with the same key.
- Notification channel
- An Android category with its own sound and importance that users can mute separately.
- Doze
- Android's power-saving mode that delays network activity on idle devices, affecting normal-priority messages.
- Notification Service Extension
- An iOS app extension that can modify a notification before display, e.g. decrypt text or attach images.
- dSYM
- An iOS debug symbol file that turns memory addresses in crash reports into readable function names and lines.
- Crash-free users
- Percentage of users who had no crash in a period; the main release-health metric in Crashlytics.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.