Skip to main content
Version: Latest

Push Setup — Firebase Cloud Messaging on iOS and Android

Everything between "the app has no push" and "a notification lands on a real handset": Firebase project registration, the platform config each OS needs, the APNs key iOS cannot work without, getting a token, and handling a message in all four app states.

The backend is done and live — FCM credentials verified, project adolescent-social, GET /api/notifications/health reports channels.push: true. Nothing on this page needs a backend change. What the server still has is devices.total: 0 — no token has ever been registered, so no push has ever been deliverable to anyone.

The API call itself is documented in the notifications guide §0; the payload you receive is §5.1 and the deep links you route on are §5.2. This page is the platform work around them.

What's new​

  • 2026-09-24 — New page. Push had a documented API call and no documented client setup, so nothing said how getToken() is supposed to return anything. Covers Firebase console registration, google-services.json / GoogleService-Info.plist, the APNs auth key iOS is blocked on, Android 13's POST_NOTIFICATIONS, the notification channel and icon Android needs to render properly, the four app states a message can arrive in, and a verification checklist with the symptom of each failure.

0. What you need before you start​

ThingWho has itNeeded for
Access to the Firebase project adolescent-socialbackend ownerRegistering the apps, downloading config files, uploading the APNs key
iOS bundle id and Android package nameTBC — confirm before you register anythingThey must match what you ship; a mismatch mints tokens the server will immediately disable
Apple Developer account with admin rightsTBC — name the ownerMinting the APNs auth key (.p8). This is the long-pole item for iOS
A physical iOS deviceyouPush on the iOS Simulator is unreliable for FCM; treat "works on device" as the bar

One project, one app per platform. A token minted against a different Firebase project is rejected by our sends, and the server then marks the device disabledAt — so the symptom of "wrong project" looks like the device silently unregistering itself minutes after you registered it.


1. Install the SDK​

npm i @react-native-firebase/app @react-native-firebase/messaging
cd ios && pod install

@react-native-firebase/app is required even though you only use messaging — it is what reads the config files below. Pin both to the same version.


2. Android​

2.1 Register the app and drop in the config​

  1. Firebase console → project adolescent-social → Add app → Android.
  2. Package name must be exactly the one you ship (see §0).
  3. Download google-services.json → android/app/google-services.json.

2.2 Gradle​

android/build.gradle:

buildscript {
dependencies {
classpath("com.google.gms:google-services:4.4.2")
}
}

android/app/build.gradle:

apply plugin: "com.google.gms.google-services"

2.3 The runtime permission (Android 13+)​

On API 33+ a user who has not granted POST_NOTIFICATIONS sees no notification, with no error anywhere — the pushes simply go nowhere. Whether messaging().requestPermission() covers this for you depends on your @react-native-firebase version, so request it explicitly: the call is idempotent, and doing it yourself is correct on every version.

android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

and at runtime, before registering the token:

import { PermissionsAndroid, Platform } from "react-native";

if (Platform.OS === "android" && Platform.Version >= 33) {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return; // nothing to register
}

2.4 Channel and icon — do not skip these​

Our server does not set a notification channel id on the message, so Android falls back to whatever you declare as the default. Without it your notifications land in an unnamed low-importance channel and never appear as heads-up banners. Same story for the icon: without a declared one, Android uses the app icon and renders it as a white square.

AndroidManifest.xml, inside <application>:

<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="clustr_default" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_color" />

Create the channel once at startup (importance HIGH, so it can interrupt):

// e.g. with notifee
await notifee.createChannel({
id: "clustr_default",
name: "Notifications",
importance: AndroidImportance.HIGH,
});

ic_notification must be a white-on-transparent silhouette — Android tints it.


3. iOS​

3.1 Register the app and drop in the config​

  1. Firebase console → Add app → iOS, with the bundle id you ship.
  2. Download GoogleService-Info.plist and add it to the Xcode target (drag into Xcode, tick the target — copying the file into the folder is not enough).

3.2 The APNs auth key — the thing iOS is actually blocked on​

Firebase cannot talk to Apple on your behalf without it. Every iOS push fails until this is uploaded, no matter how correct everything else is, and our server logs will say the send succeeded.

  1. Apple Developer → Certificates, Identifiers & Profiles → Keys → +.
  2. Enable Apple Push Notifications service (APNs), create, download the .p8 — it can be downloaded exactly once. Note the Key ID and your Team ID.
  3. Firebase console → Project settings → Cloud Messaging → iOS app → APNs Authentication Key → upload the .p8 with its Key ID and Team ID.

One key covers development and production, and all apps on the team. Prefer it over APNs certificates, which expire annually.

3.3 Capabilities​

In Xcode, on the app target → Signing & Capabilities:

  • Push Notifications
  • Background Modes → tick Remote notifications

Both are required; the second is what lets a notification wake the app enough to be delivered while backgrounded.

3.4 Registration order​

@react-native-firebase/messaging handles the APNs token → FCM token exchange for you. You only need to ask for permission before requesting the token:

const status = await messaging().requestPermission();
const ok =
status === messaging.AuthorizationStatus.AUTHORIZED ||
status === messaging.AuthorizationStatus.PROVISIONAL;

If getToken() rejects or hangs on iOS, it is almost always §3.2 or §3.3, not your code.


4. Get the token to us​

Covered in full in the notifications guide §0 — the body, the limits, and the four moments to call it (after login, after the permission grant, on every launch, and on every onTokenRefresh, which is the one that gets missed). In short:

const token = await messaging().getToken();
await api.post("/api/devices", {
token,
platform: Platform.OS === "ios" ? "IOS" : "ANDROID",
appVersion: DeviceInfo.getVersion(),
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});

messaging().onTokenRefresh(registerDevice); // reinstall, restore, app-data clear, FCM rotation

Idempotent on the token, so calling it unconditionally is safe. DELETE /api/devices/:id on logout.

A user's pushes fan out to at most 20 active devices, most-recently-seen first. Dead tokens are disabled server-side on the next send, so you never have to clean up after an uninstall.


5. Receiving a message​

5.1 What arrives​

Our messages carry both a notification block and a data block — see §5.1 of the notifications guide for every key. The practical consequence of carrying both:

  • Background / quit: the OS draws the notification itself. You do nothing.
  • Foreground: the OS draws nothing, on either platform. If you want a banner while the app is open, you render it (notifee or equivalent) from onMessage.

Everything you route on is in data, and FCM delivers every data value as a string — data.type tells you what happened, data.deepLink where to go.

5.2 The four states, all of which you must handle​

App stateHandlerNotes
Foregroundmessaging().onMessage(cb)Nothing is displayed for you. Render it or drop it.
Background, data processingmessaging().setBackgroundMessageHandler(cb)Android only in practice — our payload does not set content-available, so iOS does not wake your JS for it. Don't put anything load-bearing here.
Background → tappedmessaging().onNotificationOpenedApp(cb)Route on data.deepLink.
Quit → tapped (cold start)messaging().getInitialNotification()Call once on boot. Miss this and taps from a killed app open the home screen.
// register at module scope, outside any component
messaging().setBackgroundMessageHandler(async () => {});

messaging().onNotificationOpenedApp((m) => route(m.data));
messaging().getInitialNotification().then((m) => m && route(m.data));
messaging().onMessage((m) => showLocalNotification(m)); // foreground only

function route(data) {
if (data?.deepLink) openDeepLink(data.deepLink); // same parser as the deep-links guide
}

data.deepLink is always present and always correct — the full table of shapes is §5.2 of the notifications guide, and registering the clustr:// scheme is §3 of the deep-links guide.

5.3 Badges and collapsing, for free​

  • iOS badge is set by the server (Activity unread + chat unread). Do not also increment it yourself or it double-counts. Android gets no badge — do it from GET /api/notifications/unread-count if you show one.
  • Tray entries collapse: repeat events on one thing replace each other instead of stacking, and within an hour the title aggregates ("Alice and 4 others liked your post."). Chat collapses per conversation. Nothing to implement.

6. Verify it actually works​

In order. Each step has one failure symptom worth recognising.

  1. Token minted. Log getToken(). Empty or throwing on iOS → §3.2 / §3.3.
  2. Server has it. GET /api/devices → your row, with the id you keep for logout. Empty → the call never fired (check it runs after login).
  3. Server is healthy. GET /api/notifications/health → channels.push: true and devices.total ≥ 1. push: false is our problem, not yours — tell the backend; it means the FCM credential does not resolve.
  4. Trigger one. From another account, like one of your posts (or follow you). Background the app first.
  5. It lands. Tray entry within a second or two. Tap it → the right screen.
SymptomCause
Nothing on Android 13+, no error anywherePOST_NOTIFICATIONS never granted (§2.3)
Android notification is silent / no banner, white square iconNo default channel or icon meta-data (§2.4)
Android fine, iOS silentAPNs auth key not uploaded (§3.2) — the single most common cause
Your device shows disabledAt set within seconds of registeringToken minted against a different Firebase project, or the app was uninstalled
getToken() never resolves on iOSSimulator, or missing Push Notifications / Background Modes capability
Nothing while the app is open, fine when backgroundedWorking as designed — foreground display is yours to render (§5.1)
Notification shows, tap opens the home screengetInitialNotification / onNotificationOpenedApp not wired, or the clustr:// scheme not registered
Push stops for one user after a reinstallonTokenRefresh not registered (§4) — the classic

⤓ Download .md