Sending a Push Notification from a Firebase Function

An essential part of maintaining user engagement with mobile applications is sending push notifications at the appropriate time.  Push notifications come in two flavors:

  • Local push notifications: notifications scheduled by the app itself, based on internal events.  For example, a simple timer app could schedule a push notification to alert the user when a timer has completed.
  • Remote push notifications: notifications sent from backend server processes when some external event has occurred.

This post will focus on sending remote push notifications via Firebase Cloud Messaging (FCM) when the backend process is a Firebase cloud function.  The concepts here can be adapted to a backend hosted in a different environment (AWS, on-premises servers, etc.). Those scenarios are supported by FCM but are outside the scope of this post.

How Firebase Cloud Messaging Works

Firebase Cloud Messaging (FCM) is Google's solution for sending push notifications to applications running on multiple platforms (e.g. Apple iOS, Android, etc.).  While it is possible to write backend code to interface directly with proprietary systems like Apple Push Notification service (APNS), a middleware service like FCM reduces the complexity of the backend implementation and provides helpful out-of-the-box features that would be significant effort to develop in-house.

FCM provides a single API for a backend process to call to send notifications to multiple platforms.  Most of the time, the backend sending the push notification sends a single JSON formatted notification payload, and FCM knows which proprietary system (APNS, GCM, etc.) to route it to for delivery. I say most of the time because it is possible to include message properties that are platform-specific, but most requirements can be met using cross-platform payloads.

Backend Authentication

Sending outbound push notifications is a privileged operation that we must be sure is only done by backend code we trust.  As such, Firebase requires that code sending FCM push notifications is authorized to use the Firebase Admin SDK. This authorization is accomplished in two ways:

  1. Code that runs within a Firebase Cloud Function is implicitly trusted to use admin APIs, since this code already runs in the Firebase project security envelope.  This post will focus on this scenario.
  2. Code that runs in a hosting environment outside Firebase (e.g. AWS, on-premises servers) must be authenticated to use the Firebase Admin SDK, typically with server-to-server OAuth.  This is beyond the scope of this post, but is described in the Firebase documentation.

What Targets Are Possible

Firebase messages can be sent to single devices or sets of devices. Messages can be sent to:

  • Single devices, for example a single instance of the app running on a specific device.
  • Groups of devices, which are essentially an array of device identifiers.
  • Devices that have subscribed a topic, such as all user devices that subscribed a news about a specific sports team.

To send to a single device or a group of devices, each device will send a unique token to our backend, and our backend must store the token for later use in sending notifications.  It's common to store a token in a user profile, for example.

Writing a Send Function

This post assumes familiarity with writing Firebase Cloud Functions (which are basically Node.js functions deployed to the Google Cloud Function service wrapped under the covers).  The following code is a simple function to send a push notification to a single device:

const functions = require('firebase-functions')

// 1
const admin = require('firebase-admin')

// NOTE: the caller of this exported onCall function should be authenticated in production!
// For example, either secure the function using Firebase authorization rules,
// or use an X-Auth-Token stored in Google Secret Manager.
exports.sendPushWithDeviceId =
  functions.https.onCall(async (data, context) => { 
    try {
      // 2
      const payload = {
        notification: {
          title: data.title,
          body: data.body,
        },
        // 3
        token: data.token,
      }
      
      // 4
      const response = await admin.messaging().send(payload)

      return {
        success: true,
        response: response,
        errorMessage: null,
      }
    } catch (error) {
      log.error(error)
      return {
        success: false,
        response: null,
        message: error.message,
      }
    }
  })

This function has three important aspects to note (numbers match comments in the code):

  1. The function uses the firebase-admin JavaScript library to send the payload to the device, so importing this library is a prerequisite
  2. The payload for the message is JSON.  This payload basically follows the conventions set by Apple APNS and Google GCM.  Payload can include standard properties, and custom data properties.  See the Firebase documentation for more details.
  3. Firebase messages sent to specific devices are routed using a Firebase token, which the device would have previously sent to the backend typically via a POST or PUT api when the app launches.  If a user declines to receive push notifications when prompted by iOS, Android or a web browser, the backend will notifications will not be delivered to to that user's device.
  4. Here the admin API is called to send the message

Calling the Function

The function in the previous example is a POST function that could be called by an anonymous caller without any authentication.  This should not be done in production.  I've kept it this simple to focus only on the code relevant to sending a message while providing an example easy to test in development.

For more information about using Google Secret Manager to secure a function like this with an X-Auth-Header, see this related post.

Of course, if this function didn't need to be called by an external (web) caller, it could be implemented as a non-published JavaScript function called only from within the Firebase backend, for example:

async function sendPushWithDeviceId(token, title, body) {
  ...
}

With that said, here's an example to call this function as a Firebase onCall function via a cURL command line:

curl -X "POST" "https://{project-name}.cloudfunctions.net/sendPushWithDeviceId" \
     -H 'Content-Type: application/json' \
     -d $'{
  "data": {
    "token": "{********[ User FCM Token Here ]********************}",
    "title": "Test Push Notification",
    "body": "This is a notification message. "
  }
}'

Upon posting the request to the Firebase function, the message will be sent from the function to Firebase Cloud Messaging (FCM).  FCM will then route the message to the appropriate proprietary push notification service, e.g. APNS for Apple devices, and Google Cloud Messaging for Android devices.

Next learn how to add server-to-server authentication to the function.