Skip to main content

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:

  1. the mobile app registers a device token,
  2. the backend stores that token under the signed-in user,
  3. a business event happens,
  4. the backend decides which user or device should receive a notification,
  5. the backend sends the notification through Firebase Admin SDK,
  6. 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:

  • notification for the system banner
  • data for 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 notification
  • data: carries app-specific routing information such as /settings
  • android: improves Android delivery behavior
  • apns: tells APNs to deliver the iOS push as a visible notification
  • webpush: 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:

  1. keep the app in the background,
  2. send the message,
  3. check that the system banner appears,
  4. tap the banner,
  5. 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.url routed 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:

  1. backend sends data.url,
  2. iOS shows the system banner,
  3. the user taps the notification,
  4. Flutter receives the notification open event,
  5. Flutter extracts message.data.url,
  6. Flutter loads that route inside the WebView.

This is why the route field matters in the payload.


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.