Sending Push Notifications by Code
This guide explains the production-style way to send mobile push notifications in the GOFA stack.
It is written for the current architecture:
- Flutter shell hosts the app and receives native push notifications
- WebView frontend handles login and registration logic
- Next.js backend sends notifications through Firebase Admin SDK
How mature apps usually do this
A mature app normally does not rely on Firebase Console for day-to-day product notifications.
The common production pattern is:
- the mobile app registers a device token,
- the backend stores that token under the signed-in user,
- a business event happens,
- the backend decides which user or device should receive a notification,
- the backend sends the notification through Firebase Admin SDK,
- the app opens the correct screen when the user taps the notification.
Firebase Console is still useful, but mainly for:
- manual testing,
- temporary broadcasts,
- operator-driven campaigns.
For product logic, code-driven sending is the standard approach.
Two GOFA sending modes
GOFA currently supports two code-driven sending patterns.
1. Send by raw token
Relevant backend file:
gofa-web-nextjs/src/app/api/fcm/send/route.ts
Endpoint:
POST /api/fcm/send
Use this when you want:
- a quick local test,
- a single-device test,
- payload validation,
- route validation such as
/settings.
Example request:
{
"fcmToken": "<device token>",
"type": "background_test_settings",
"title": "Open Settings Test",
"body": "Tap to open the Settings page",
"url": "/settings"
}
This is best treated as a debug/testing endpoint.
2. Send by user ID
Relevant backend file:
gofa-web-nextjs/src/app/api/fcm/send-to-user/route.ts
Endpoint:
POST /api/fcm/send-to-user
Use this when you want:
- production-style behavior,
- backend-controlled device selection,
- one request that reaches all active devices for a user,
- automatic cleanup of invalid tokens.
Example request:
{
"uid": "lz2ThACquXbpZMRsYHNZ78ElqB42",
"type": "open_settings_test",
"title": "Open Settings",
"body": "Tap to open the settings page",
"url": "/settings",
"platforms": ["ios"]
}
This is the preferred production API.
A full walkthrough is documented separately in:
GOFA API > Send Push Notifications to a User
Why notification payload matters
There are two important payload sections.
1. notification
This is for the operating system.
Use it when you want the phone to show a system banner.
Example:
{
"notification": {
"title": "Open Settings Test",
"body": "Tap to open the Settings page"
}
}
2. data
This is for your app logic.
Use it when you want the app to know what to do after the user taps the notification.
Example:
{
"data": {
"version": "1",
"type": "background_test_settings",
"url": "/settings"
}
}
Production rule
A production mobile notification usually sends both:
notificationfor the system bannerdatafor app behavior
That is exactly what you want in GOFA.
Current GOFA payload structure
The GOFA backend sends a mixed payload like this:
await MESSAGING.send({
token: fcmToken,
notification: {
title,
body: messageBody,
},
data: {
version: PROTOCOL_VERSION,
type,
title,
body: messageBody,
...(url ? { url } : {}),
},
android: {
priority: 'high',
notification: {
channelId: 'default',
clickAction: 'FLUTTER_NOTIFICATION_CLICK',
},
},
apns: {
headers: {
'apns-priority': '10',
},
payload: {
aps: {
sound: 'default',
},
},
},
webpush: {
headers: {
Urgency: 'high',
},
},
});
Why each part exists:
notification: tells iOS/Android to show a visible system notificationdata: carries app-specific routing information such as/settingsandroid: improves Android delivery behaviorapns: tells APNs to deliver the iOS push as a visible notificationwebpush: keeps the endpoint compatible with web push use cases
Step-by-step: how to send a push notification by code
Step 1: make sure the device is registered
Before you send, confirm the target user has a token in Firestore.
Expected structure:
Clients/{clientId}/ClientUsers/{uid}/fcmTokens
Example key:
installation:native-1772433667723-17uxidu
If no token is stored, there is nothing to send to.
Step 2: choose the right sending mode
For local testing, you can send directly to one raw token.
For production, prefer sending by uid so the backend can load all active tokens for that user.
Step 3: build the payload
For a background push with a route target:
{
"title": "Open Settings Test",
"body": "Tap to open the Settings page",
"url": "/settings",
"type": "background_test_settings"
}
Step 4: send through the backend
Debug / single-device example
curl -X POST https://www.uat.gofa.app/api/fcm/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <firebase_id_token>" \
-d '{
"fcmToken": "<device token>",
"type": "background_test_settings",
"title": "Open Settings Test",
"body": "Tap to open the Settings page",
"url": "/settings"
}'
Production-style example
curl -X POST https://www.uat.gofa.app/api/fcm/send-to-user \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin_firebase_id_token>" \
-d '{
"uid": "lz2ThACquXbpZMRsYHNZ78ElqB42",
"type": "open_settings_test",
"title": "Open Settings",
"body": "Tap to open the settings page",
"url": "/settings",
"platforms": ["ios"]
}'
Step 5: verify the result on device
Background notification test checklist:
- keep the app in the background,
- send the message,
- check that the system banner appears,
- tap the banner,
- confirm the app opens to the target page.
In the current GOFA implementation, a successful test was verified with:
- app in background,
- banner displayed on iPhone,
- tapping the notification opened the app,
data.urlrouted the WebView to/settings.
How notification click routing works in GOFA
Relevant Flutter file:
gofa-silvercare-flutter/lib/screens/webview_screen.dart
The current flow is:
- backend sends
data.url, - iOS shows the system banner,
- the user taps the notification,
- Flutter receives the notification open event,
- Flutter extracts
message.data.url, - Flutter loads that route inside the WebView.
This is why the route field matters in the payload.
Recommended production direction
Keep both APIs, but use them for different purposes.
Keep /api/fcm/send
Use it for:
- direct device testing,
- QA work,
- payload debugging.
Prefer /api/fcm/send-to-user
Use it for:
- production notification flows,
- admin panels,
- server-triggered business events,
- future multi-device delivery.
That split keeps local testing simple while keeping the production contract correct.