Sending Firebase Cloud Messages from a Node.js Server
The previous chapter created an example Firebase Cloud Messaging client app and outlined the steps involved in obtaining the registration token for an app and device combination. The tutorial then demonstrated the use of that token to send notification messages targeted to the specific device using the Notifications section of the Firebase console.
While the Firebase console is useful for manually sending downstream notification messages to client apps, more complex development projects will typically need to send messages programmatically from a server. The Firebase console is also limited to sending so called “notification” messages. While these messages can contain a data payload, they always result in a notification appearing within the Android status bar and notifications shade when the app is in the background. Server based message sending, on the other hand, allows data only messages to be sent to devices, allowing data to be delivered to client apps without triggering a notification.
As discussed in earlier chapters a number of options are available for sending messages from a server. One such option, and the topic of this chapter, involves the use of the Firebase Admin SDK and Node.js.
An Introduction to Node.js
JavaScript began as a scripting language intended to provide interactive behavior to web pages. Traditionally JavaScript code was embedded into the content of a web page, downloaded into a web browser and executed using a JavaScript engine built into the browser. In the case of the Google Chrome browser, this engine is known as the V8 JavaScript engine.
In very basic terms, Node.js takes the V8 JavaScript engine and repurposes it to provide a server-side runtime environment for executing JavaScript code. The result is an extremely fast, efficient and scalable environment in which to create and serve dynamic web content.
Node.js includes a range of modules that provide specific functionality such as networking and database access. One such module is the Firebase Admin Node.js SDK provided by Google to allow Firebase cloud messaging to be implemented within Node.js code.
In the following sections of this chapter steps will be outlined demonstrating how to install Node.js on macOS, Windows and Linux before outlining the use of Node.js to send messages using Firebase cloud messaging. Regardless of which platform you choose to use to run Node.js, think of it as fulfilling the role of the server and the Android app as the client.
Node.js can be downloaded pre-built for most platforms from the following URL:
https://nodejs.org/en/download/
Installing Node.js on macOS
From the Node.js download page, select and download the macOS Installer (.pkg) file. Once the package has downloaded, locate it in a Finder window and double-click on it to launch the installer. Work through the installation process, accepting the default selections unless you have specific requirements. On completion of the installation, both the npm and node executable binaries will be installed in the /usr/local/bin folder and will be accessible from the command-prompt within a Terminal window.
Installing Node.js on Windows
Download the appropriate Windows Installer (.msi) package (either 32-bit or 64-bit depending on your hardware platform) and launch the installer once the download is complete. Windows Installer will unpack the Node.js files and present a dialog within which the installation may be performed. Accept the license terms and choose a filesystem location into which the Node.js files should be installed. On the custom setup screen, accept the default package selection, then click Next followed by the Install button. Once the installation is complete, the files will have been installed in the selected location and will be accessible within a Command Prompt window.
Installing Node.js on Linux
The steps to install Node.js on Linux will vary depending on the Linux distribution. On Red Hat and CentOS systems, take the following steps:
yum install epel-release yum install nodejs npm --enablerepo=epel
On systems running Ubuntu, Node.js may be installed using the following command:
sudo apt-get install nodejs nodejs-legacy npm
Initializing and Configuring Node.js
Regardless of the platform on which Node.js has been installed, the same configuration steps need to be performed before Node.js is ready to send Firebase cloud messages.
Open a terminal or command prompt window and execute the following command to verify that the package was successfully installed:
node -v
Assuming that this command outputs the version of Node.js installed on the system, the next step is to create a package.json file. To achieve this, create and change to a new directory in which to work with Node.js and run the npm command as follows:
npm init
When prompted, enter information at each prompt. Since this is simply a test package, the exact information entered is not of paramount importance. The following lists some suitable options:
• name: firebasefcm
• version: 1.0.0
• description: An example package for testing Firebase FCM
• entry point: index.js
• test command: (leave blank)
• git repository: (leave blank)
• keywords: (leave blank)
• author: (your name or company)
• license: ISC
The remainder of this chapter assumes that all actions are being performed within the directory containing the package.json file.
Running a Test
Perform one final installation check by creating a new file named index.js containing the following code using an editor of your choice:
var sys = require("util"); console.log("Hello World");
These lines of code indicate that the util module is to be imported before using the console.log function to output a message to the console. Once the file has been created, execute it using the node command-line tool:
node index.js
When the code is executed, output reading “Hello World” will appear.
Installing the Firebase Admin SDK
Since the Node.js code written in the remainder of this chapter is going to make extensive use of the Firebase Admin SDK the next step is to install this module using the npm command. Remaining in the directory containing the package.json file, install the module library as follows:
npm install firebase-admin --save
A sub-directory named node_modules containing the firebase-admin module will have been created and a review of the package.json file will show that the firebase-admin module is now listed as a dependency for the current project:
. . "dependencies": { "firebase-admin": "^5.0.0" . .
Generating the Service Account Credentials
Before any Firebase cloud messages can be sent using Node.js, an additional JSON file needs to be generated and installed on the server. This file contains a private key that allows Node.js code to communicate through the SDK to the Firebase messaging service and is generated from within the Firebase console.
Open the Firebase console in a browser window, select the Firebase Examples project and select the Settings option as highlighted in Figure 27-1:
Figure 27-1
On the settings page, select the Service Accounts tab followed by the Firebase Admin SDK option:
Figure 27-2
Click on the Generate New Private Key button, download the generated file and place it in a suitable directory on the system on which Node.js has been installed. This can be in the directory containing the package.json file or any other directory where it can be referenced within Node.js code. Regardless of where the file is placed, it is important to keep this file secure to prevent others from using your account to send Firebase cloud messages. If the key security is compromised, return to the Firebase console and generate a new one.
The Firebase Admin SDK page also includes a useful Node.js snippet that will be used in the next section to perform some initialization tasks so keep this page open in the browser for now.
Initializing the Admin SDK
With Node.js installed and configured, work can begin writing code to send a message to a device. Once again using the editor of your choice, create a new file named send.js and copy and paste the Node.js snippet from the Firebase console web page into the file:
var admin = require("firebase-admin"); var serviceAccount = require("path/to/serviceAccountKey.json"); admin.initializeApp({ credential: admin.credential.cert(serviceAccount), databaseURL: "<your database URL here>" });
The code imports the firebase-admin module and then declares a variable referencing the location of the JSON file containing the private key generated earlier in the chapter. Modify the placeholder path to reference the actual name and location of the file, for example:
var serviceAccount = require("/home/neil/nodejs/firebase-adminsdk.json");
Wrapping the path in a require statement ensures that an error will be thrown if the file does not exist at the specified location.
Note also that the databaseURL for the Firebase project is included in the arguments passed to the initializeApp method. This is the URL for the realtime database associated with the project. If the initialization snippet was copied from the Firebase console this should already be set to the correct URL.
Adding the Destination Registration Token
For the purposes of this example, a message will be sent only to a specific device. Locate the registration token for the device and app combination tested in the previous chapter and assign it to a variable in the send.js file beneath the SDK initialization code:
. . var registrationToken = "<registration token goes here>";
Understanding Message Payloads
The content of a message is referred to as the payload. Firebase messaging supports notification, data and combined messages.
• Notification Messages - Consist of a title and a message body and trigger the notification system on arrival at the device. In other words, an icon will appear in the status bar and an entry will appear in the notification shade. Such notifications should be used when sending an informational message that you want the user to see.
• Data Messages - Contain data in the form of key/value pairs and are delivered directly to the app without triggering the notification system. Data messages are used when sending data silently to the app.
• Combined Messages – Contain a payload comprising both notification and data. The notification is shown to the user and the data is delivered to the app.
The following example declares a notification payload consisting of a title and a message body:
var payload = { notification: { title: "Account Deposit", body: "A deposit to your savings account has just cleared." } };
A data message, on the other hand, uses the data keyword together with one or more key/value pairs:
var payload = { data: { account: "Savings", balance: "$3020.25" } };
A combined message payload contains both notification and data elements as follows:
var payload = { notification: { title: "Account Deposit", body: "A deposit to your savings account has just cleared." }, data: { account: "Savings", balance: "$3020.25" } };
The payload may also have options associated with it. The main option keys are as follows:
• collapseKey – Used to identify a group of messages where only the most recent message is sent if multiple messages are waiting to be delivered to the device.
• contentAvailable – Used when sending messages to iOS devices. When set to true, the app is woken on message receipt. This is the default behavior for messages received on Android devices.
• dryRun – When set to true the message is not actually sent. Useful for testing purposes during development.
• mutableContent – Applies only to iOS client apps and when set to true allows the app to modify the message content before it is presented to the user as a notification.
• priority – A string value that may be set to “high” or “normal”. By default notifications are high priority while data messages default to normal priority. Normal priority messages may be delayed but impose a lower burden on the device battery. High priority messages, on the other hand, are sent immediately, wake sleeping devices and open a network connection to the corresponding server.
• timeToLive – A numeric value indicating the amount of time in seconds that the message should be held if the destination device is offline. This can be set to any duration up to four weeks (2419200 seconds). If no TTL is specified the default is four weeks.
The following is an example option declaration that sets the priority to normal with a time to live duration of one hour:
var options = { priority: "normal", timeToLive: 60 * 60 };
Defining the Payload and Sending the Message
Edit the send.js file and add the following payload and options declaration beneath the registration token variable:
var payload = { notification: { title: "This is a Notification", body: "This is the body of the notification message." } }; var options = { priority: "high", timeToLive: 60 * 60 *24 };
All that remains is to send the message. This involves a call to the sendToDevice() method of the Firebase Admin SDK, passing through the registration token, payload and options and then checking the response to find out if the message was sent successfully:
admin.messaging().sendToDevice(registrationToken, payload, options) .then(function(response) { console.log("Successfully sent message:", response); }) .catch(function(error) { console.log("Error sending message:", error); });
Testing the Code
Run the Messaging app created in the previous chapter on the device or emulator session that matches the reference token included in the message and place it in the background. Within the terminal or command prompt window, run the following command:
node send.js
After a short delay, output similar to the following should appear indicating that the message was send successfully:
Successfully sent message: { results: [ { messageId: '0:1493066541057227%00ae8e2b00ae8e2b' } ], canonicalRegistrationTokenCount: 0, failureCount: 0, successCount: 1, multicastId: 7678894013550453000 }
Refer to the device or emulator and note the appearance of the notification icon in the status bar. Drag down from the status bar to reveal the notification shade and tap on the notification sent from the Node.js server. The notification body text should now appear in the TextView in the main activity.
Next, change the payload in the send.js file to a data message:
var payload = { data: { MyKey1: "Hello" } };
Send the message again with the app in the foreground and note that the data is output to the Android Studio logcat panel. Send the message once more, this time with the app in the background. Note that no notification is triggered but that the value is once again displayed in the logcat output. Clearly the message was still received even though no notification was triggered on the device.
Topics
Firebase FCM topics offer developers a publish and subscribe system that allows apps to opt-in to receiving messages within certain categories. A news app might, for example, provide the user with the option of subscribing to notifications for different categories of news headlines such as business, local and international news.
A client app can be subscribed to a topic either from within the app itself, or on the server using the app’s registration token.
A client app subscribes itself to a topic by making a call to the subscribeToTopic() method of the FirebaseMessaging instance. If the topic does not already exist, the method call creates the topic. The following code, for example, subscribes an app to a topic named “finance”:
FirebaseMessaging.getInstance().subscribeToTopic("finance");
A call to the unsubscribeFromTopic() method of the FirebaseMessaging instance (passing through the topic string as an argument) will opt the app instance out of receiving further messages for that topic.
FirebaseMessaging.getInstance().unsubscribeFromTopic("finance");
To subscribe an app instance from the server using Node.js, a call must be made to the Admin SDK subscribeToTopic() method passing through the registration token of the app/device combination and the name of the topic:
var registrationToken = "<registration token here>"; var topic = "finance"; admin.messaging().subscribeToTopic(registrationToken, topic) .then(function(response) { console.log("Successfully subscribed to topic:", response); }) .catch(function(error) { console.log("Error subscribing to topic:", error); });
Multiple app instances may be subscribed to a topic by passing through an array of registration tokens as follows:
var registrationTokens = [ "<registration token one>", "<registration token two>", … ]; var topic = "finance"; admin.messaging().subscribeToTopic(registrationTokens, topic) .then(function(response) { console.log("Successfully subscribed to topic:", response); }) .catch(function(error) { console.log("Error subscribing to topic:", error); });
To opt client apps out from a topic using Node.js on the server, simply call the unsubscribeFromTopic() method passing through a registration token (or array of tokens) together with the topic name string. Messages may be sent to subscribed devices either from a server, or using the Notifications panel of the Firebase console. To send a notification to topic subscribers within the Firebase console, compose a new notification message and select the Topic option within the Target section of the message composition screen. With the Topic option selected, type the first few letters of the topic name into the text field to see a list of matching topics to which the message is to be sent:
Figure 27-3
Note when using the Firebase console to send notifications to topic subscribers that it can take up to 24 hours for a newly created topic to appear within the topic list.
To send a message to subscribed client apps from a server using Node.js, the sendToTopic() Admin SDK method is called passing through the payload and topic string. In the following code fragment, a notification message is sent to all “finance” topic subscribers:
var payload = { notification: { title: "NASDAQ News", body: "The NASDAQ climbs for the second day. Closes up 0.60%." } }; var topic = "finance"; admin.messaging().sendToTopic(topic, payload) .then(function(response) { console.log("Successfully sent message:", response); }) .catch(function(error) { console.log("Error sending message:", error); });
When sending messages to a topic, Firebase supports the use of regular expressions ([a-zA-Z0-9-_.~%]+) to target multiple topics based on matching criteria. Assuming three topics named news_local, news_business and news_politics, for example, the following expression would send a message to all three topics:
var topic = "news_*"; admin.messaging().sendToTopic(topic, payload) . .
Using Topic Conditions
Topic conditions may also be used when sending messages to topics. Conditions allow the sender to define the terms under which a subscriber is eligible to receive a message. This is defined based on the topics to which the app is subscribed.
Topic conditional expressions support both the AND (&&) and OR (||) operators and are used with the sendToCondition() Admin SDK method. In the following code, for example, the message will be received by an app only if it has subscribed to the news topic while also being subscribed to either the finance or politics topics:
var condition = "'news' in topics && ('finance' in topics || 'politics' in topics')"; admin.messaging().sendToCondition(condition, payload) .then(function(response) { console.log("Successfully sent message:", response); }) .catch(function(error) { console.log("Error sending message:", error); });
When working with topic conditions, the conditional expressions are limited to two conditional statements.
Summary
This chapter has outlined and demonstrated the use of Node.js and the Firebase Admin Node.js SDK to send and manage Firebase cloud messages from a server environment. This involves the installation of the Node.js environment and Admin SDK configured with the appropriate Firebase account credentials. When an app is installed on a device it is assigned a registration token which uniquely identifies that combination of device and app. Using this token, messages can be targeted to specific devices from the server. Messages may also be targeted to devices where the app has subscribed to specific topics. As will be outlined in the next chapter, messages may also be sent to multiple devices through the use of device groups.