Building a Robust Notification System in React Native (FCM + Notifee + Real-Time Sync)
Android Push Notifications Architecture

When I first started implementing push notifications in a React Native app, I assumed it would be straightforward.
"Just integrate Firebase and done."
That assumption lasted about 10 minutes.
What I eventually built was not just push notifications - but a complete notification system that works across:
Foreground
Background
Killed app state
Interactive actions (Approve / Reject)
This article walks through that system - what works, what breaks, and what you actually need in production.
The Stack I Used
Before diving in, here's the setup:
Firebase Cloud Messaging : receives notifications from backend
Notifee : displays notifications with custom UI and actions
Socket.io : keeps data fresh when app is open
The Architecture (This is where things get interesting)
Push notifications aren't just "receive and show".
They're a pipeline.
Backend → FCM → App → Notifee → User → Action → Backend
Step 1: Setting Up Firebase (The Missing Piece Most Docs Skip)
Before anything works, you need to connect your app to Firebase.
Go to Firebase Console
Create a project
Add Android app
Use your package name
Download google-services.json
Place it here:
android/app/google-services.json
- Then connect it using Gradle:
// android/build.gradle classpath
'com.google.gms:google-services:4.3.15'
// android/app/build.gradle apply plugin:
'com.google.gms.google-services'
Without this, nothing else matters. Seriously.
Step 2: Notification Channels (Android Reality Check)
If you've never dealt with Android notification channels, here's the catch:
Once a channel is created, you cannot change it.
So if you mess up sound or importance ... Then, you're stuck.
The workaround?
Version your channel:
default_sound_v0 → default_sound_v1 → default_sound_v2 ...
Every time you need changes → create a new version.
Step 3: Custom Notification Sounds
I wanted a branded notification sound, so I added:
notification_sound.wav
Placed here:
android/app/src/main/res/raw/
And this line saved me hours:
noCompress += ['wav', 'mp3', 'ogg']
Without it, the sound randomly fails on real devices.
Step 4: Handling All App States
This is where most implementations break.
Foreground
FCM does NOT show notifications automatically.
You must handle it manually:
onMessage → display notification using Notifee
Background / Killed App
Handled in:
setBackgroundMessageHandler
This runs even when your app is not open.
Step 5: Interactive Notifications (Game Changer)
This is where things feel "premium".
Instead of just showing a message, I added:
Approve
Reject
When user taps:
deliveryAuthorizationService.takeAction(id, { action })
Now notifications are not passive . Now, they're actionable.
Step 6: Event-Based System (Clean Architecture)
Instead of handling everything in one place, I mapped notifications to events:
AUTH_DELIVERY → open modal + refresh data
RATE_UPDATED → update UI
SYSTEM_ALERT → show alert banner
This keeps the system scalable.
Step 7: Token Registration (Don't Skip This)
Notifications won't work unless your backend knows the device.
Flow:
Request permission
Get token
Send to backend
getToken()
registerDeviceToken()
Things I Learned the Hard Way
Duplicate notifications happen if backend sends both notification and data
Android channels cannot be edited after creation
Foreground notifications must be handled manually
Custom sounds break if compressed
Missing google-services.json = silent failure
Final Thoughts
Push notifications are not a "feature".
They're a system.
Once you treat them like one, separating:
Receiving
Displaying
Handling actions
Syncing data
Everything starts to make sense.
And more importantly, it becomes reliable.
If you're building something similar, focus less on "making it work" and more on "making it predictable".
That's the real difference between a demo and a production system.
Note: A few images in this article are AI-generated to help visualize the concepts more clearly.
