Push and messaging · from our own builds

Firebase Cloud Messaging integration: push notifications that actually arrive on Android

Integrating FCM takes a server-side sender built on the Firebase Admin SDK, a token table that knows which account and which app each phone belongs to, Android notification channels and the Android 13 permission set up before the first release, and clean-up of dead tokens. Sending is free; the work is in delivery. In Pakka the first send would have failed outright on firebase-admin 14 even though every push test passed, and a missing permission would have dropped every notification in the customer app.

Updated 20 October 2026 · checked against our code and the vendors' documentation

Where have we built this?

  1. PakkaTrip requests as a loud heads-up with their own sound, booking updates and operations alerts, live on both production backends since 16 September 2026 while the apps go to testers as signed APKs.
  2. LeafRangerOrder and dispute events and a watering reminder backstop, run end to end on an Android 16 emulator against the real Firebase project in August 2026, and not re-tested since the app's package name changed in September. The app is not in a store yet.
  3. LetSlicLikes, comments, follows, mentions and messages, shown by notifee while the app is open and routed to the right screen on tap.

What are the numbers and limits to know?

Price
Cloud Messaging (FCM) is listed as no-costsource, checked 20 October 2026
Payload size
4,096 bytes maximum for both notification and data messages; the Firebase console allows 1,000 characterssource, checked 20 October 2026
Send quota (HTTP v1)
600,000 messages per minute per project by default; over quota returns HTTP 429source, checked 20 October 2026
Priority on Android
Normal is the default for data and notification messages; high priority can wake a sleeping devicesource, checked 20 October 2026
Deprioritisation
High-priority messages that don't lead to a visible notification may be dropped to normal priority, judged per app install over 7 days; if the user has turned notifications off, messages are deprioritisedsource, checked 20 October 2026
Token expiry
An Android registration inactive for 270 days is treated as expiredsource, checked 20 October 2026
Errors that mean 'delete the token'
UNREGISTERED (HTTP 404); INVALID_ARGUMENT (HTTP 400) only when you are sure the payload is validsource, checked 20 October 2026
Android notification channels
Once a channel is created, its sound, importance and other behaviours can't be changed by the app; only its name and descriptionsource, checked 20 October 2026
Android 13 permission
Android 13 (API 33) and later need the POST_NOTIFICATIONS runtime permission, declared in the manifestsource, checked 20 October 2026

How do you integrate Firebase Cloud Messaging, step by step?

  1. 1

    Register each app in one Firebase project

    Pakka has a customer app and a driver app, each registered by package name. The Firebase config file stays out of the repository, and the server's service-account key lives outside the codebase, installed as one environment value per backend.

  2. 2

    Send from the server with the Admin SDK's modular API

    Both of Pakka's backends and LeafRanger's import firebase-admin/app and firebase-admin/messaging, initialise an app from the service account and send with sendEachForMulticast. The sender stays inert, and says so, when the credential is missing.

  3. 3

    Store tokens by account and by app

    Pakka's device-token table keys each unique token to a user and an app, so a person who is both customer and driver gets driver payouts in the driver app only. Registering a phone replaces that account's older token for the same app, and onTokenRefresh re-registers.

  4. 4

    Create the channels before the first public build

    Pakka's driver app has a trip-request channel at high importance with its own sound and vibration pattern, plus trip-update and account channels. Android will not let the app change a channel's sound later, so the channel ids carry a version suffix.

  5. 5

    Ask for permission after sign-in, and show when it is off

    POST_NOTIFICATIONS is declared in both Pakka apps and requested right after sign-in. A driver who goes online with notifications off sees 'Trip alerts are off' with a button that opens the app's notification settings.

  6. 6

    Set priority and lifetime per message

    Trip requests go at high priority with a time-to-live equal to the request's lifetime and a shared tag, so a newer request replaces an expired one in the shade. Other trip updates also go at high priority; the rest go at normal priority.

  7. 7

    Handle foreground, background and tap

    Our apps send notification messages with a data payload. Android shows them itself when the app is in the background or killed. While the app is open, LetSlic shows them with notifee, and Pakka rings trip requests with a looping notifee alert because they were silent in the foreground. A tap reads the data and opens the right screen, including a cold start straight into a driver's trip popup.

  8. 8

    Delete tokens when they die or the user leaves

    Pakka deletes a token only on registration-token-not-registered or invalid-registration-token. Logout unregisters the device before clearing the session, and suspension or account erasure removes every device.

What went wrong, and how was it fixed?

Push that could never have worked, with green tests

firebase-admin 14 has only the modular API, so admin.credential.cert and admin.messaging() do not exist and the first real send would have failed. Both push suites passed because their fake firebase-admin had the old shape.

Fix: Fixed in both backends on 16 September 2026 with the modular imports. The fakes now check the real module's exports before standing in for it.

Channels created silent

notifee's default channel sound is none, so the driver's trip-update and account channels were created without sound. Android never changes a channel's sound after it exists, so a release with silent channels stays silent.

Fix: Set sound to 'default' in both apps before the first release, cleared the app to recreate the channels, and versioned the channel ids so a future change gets a new channel.

A missing permission in the manifest

Pakka's customer app never declared POST_NOTIFICATIONS, so on Android 13 and later every notification would have been dropped.

Fix: Declared in the customer manifest and requested after sign-in, verified on a fresh install.

One bad payload deleting all of a user's tokens

Pakka's first version treated messaging/invalid-argument as a dead token. That error can mean a bad payload, so one malformed message would have wiped every phone a user had.

Fix: invalid-argument was taken off the prune list; only not-registered and invalid-registration-token delete a token.

Pushes that ignored which app a token belonged to

A person registered as both customer and driver on one number would have received driver payout alerts in the customer app.

Fix: Tokens are stored with the app they belong to, and every one of the 19 call sites names its audience.

A sender nobody called

In LeafRanger the push service and a live FCM credential existed, the token endpoint existed and the app had a tap handler, but nothing had ever called sendToUser.

Fix: Push now goes out from the single notify() seam after the in-app notification is written, so every event that notifies also pushes, and then we ran it end to end on an emulator.

A reminder that fired every hour

LeafRanger's watering backstop checked the last reminder against a due date its query never filled in, so the guard never matched and the hourly sweep pushed the same plants every hour.

Fix: Caught by a test asserting that a second sweep changes nothing. The guard now checks a field the sweep writes itself, with a 24-hour cooldown, and skips anyone who has opened the app since the reminder was due.

What should you check before going live?

  • firebase-admin used through its modular API; test fakes match the real exports
  • POST_NOTIFICATIONS declared in every app's manifest
  • Channel sounds and importance final before the first public build
  • Tokens stored per account and per app, removed on logout
  • Only not-registered and invalid-token errors delete tokens
  • High priority used only for messages that show a notification
  • A real push seen with the app open, in the background and killed
  • Service-account key kept out of every repository

Questions about Firebase Cloud Messaging

Does Firebase Cloud Messaging cost anything to send?

No. Firebase lists Cloud Messaging as no-cost, with a default quota of 600,000 messages a minute per project. What it costs is engineering time: tokens, channels, permissions and testing on real devices.

Why do push notifications stop arriving on Android 13 and later?

Most often because the app never declared or requested POST_NOTIFICATIONS. Pakka's customer app had exactly that gap before release. FCM's docs add that messages to an install with notifications turned off are deprioritised, so check permission before sending urgent pushes.

Can a push notification ring like a call for a delivery driver?

Pakka chose a loud heads-up notification with its own sound rather than a full-screen alert. Its trip-request channel has high importance, its own sound and a vibration pattern, and with the app open a looping alert plays while the request is on screen.

Should the server send notification messages or data-only messages?

Our three apps send notification messages with a small data payload. Android displays them itself when the app is in the background or killed, and the data carries the ids the app needs to open the right screen. While the app is open, Pakka and LetSlic show them with notifee.

What happens to push tokens when a user logs out or changes phone?

In Pakka, logout unregisters the phone before the session is cleared, and signing in on a new phone replaces the old token for that app. FCM itself treats an Android token unused for 270 days as expired.

How do you test push notifications before launch?

In stages. Pakka's server credential was checked with a dry run against a fake token on the production box, then real pushes were walked on an emulator with the app open, in the background and killed. LeafRanger ran all seven steps from permission prompt to tap on an Android 16 emulator with Play services.

Need Firebase Cloud Messaging in your app?

We have built it in production code, including the parts that went wrong. Tell us what your app needs and we'll say what it takes.

Ask about this integration