Device Group Management With Firebase Cloud Messaging

April 15, 2018

Firebase Cloud Messaging (FCM) is a free cross-platform messaging solution that lets you deliver push messages to your client apps. Using FCM you can send push notifications to individual devices, to groups of devices, or to devices subscribed to specific "topics".

This article will outline your options for managing multiple devices per user.

Whether you're targeting individual devices or groups of devices, you need to store their registration tokens -- unique IDs that are generated by the FCM SDK for each app instance.

You also need to delete the device's token when the user logs out.

Lastly, you need to delete tokens that are no longer valid. The user may uninstall your app, and FCM may regenerate a device's token. In both cases the token should be deemed invalid and should no longer be targeted for messaging. If you route messages to invalid tokens, FCM will respond with an error.

The simplest strategy is to only store one token per user in your database. Whenever you obtain a device's registration token, send it to your server and store it. When the user logs out, delete the token. When a notification fails because it was targeted to a token that has become invalid, FCM will respond with an error message and you can safely delete it from your database.

However, this won't work very well if you have users with multiple devices; only one device will receive each push message.

To send a notification to all devices associated with a user, you need to store the token of each device. You have two options:

  1. Manage tokens yourself. This entails maintaining a list of tokens for each user, storing and deleting them as appropriate, and removing tokens that eventually become invalid.
  2. Use FCM's device group messaging. This feature lets you add multiple tokens to the same "group", and route messages to the group instead of to each individual device.

Managing Tokens Yourself

Managing tokens yourself means storing them in your own database and deleting them when necessary.

Whenever your client app obtains a registration token, you need to send it to your server and store it in your database along with the rest of the user's data. Using a relational database, this can be achieved with a one-to-many relationship between a user and his/her tokens. In a document store, you can save the tokens as an array in your user's document.

You will need to make sure that no duplicate tokens are stored to prevent messages from being sent to the same device more than once. And when the user logs out, you should delete the specific device's token. This can be done, for example, by including the current token in the body of the logout request, or in a separate request.

After a user has logged into your app, and whenever the token is refreshed (i.e. regenerated by Firebase), you'd do something like this (the example code is in Javascript, but every FCM SDK contains equivalent functionality):

messaging.getToken().then(function(currentToken) {
  if (currentToken) {
    sendTokenToServer(currentToken);
    // ...
  } else {
    // ...
  }
});

When a user logs out, you should send the token to your server for deletion:

deleteTokenFromServer(currentToken);

This takes care of storing tokens when they're retrieved and deleting them on logout. But tokens will eventually become outdated and invalid, which you won't notice until you actually send a message. This usually happens when a user has uninstalled your app, switched devices or revoked or renewed push notification permissions. In this case, the response from FCM will indicate that the token is invalid, thus prompting you to delete it from your database.

How you delete invalid tokens depends on whether you're using the "legacy" HTTP protocol or the new "v1" HTTP protocol.

The Legacy FCM HTTP protocol

In late 2017 Firebase announced the new "HTTP v1" protocol for sending messages. The "legacy" protocol is still being used for various reasons, one of which is its support for "device groups" and multicast messages.

Multicast messages are messages that are sent to multiple device tokens using one request to the FCM server:

POST https://fcm.googleapis.com/fcm/send
Content-Type:application/json
Authorization:key=AIzaSyZ-1u...0GBYzPu7Udno5aA

{
  "notification": {
    "body": "Hello!"
  },
  "registration_ids" : ["token1", "token2"]
}

A successful response looks like this:

{
  "multicast_id": 108,
  "success": 2,
  "failure": 0,
  "results": [
    { "message_id": "1:08" },
    { "message_id": "1:09" }
  ]
}

In this case the message was delivered to both devices. However, if one of the tokens were invalid, the response would look like this:

{
  "multicast_id": 108,
  "success": 1,
  "failure": 1,
  "results": [
    { "message_id": "1:08" },
    { "error": "InvalidRegistration" }
  ]
}

This response doesn't tell you which token failed, but that it was the second one you provided. In other words, your server needs to know which tokens it specified, and in what exact order.

For example, in the official Cloud Functions example repository there's a sample function in which a user's tokens are stored as an array in a document store. If one or more messages fail, the results array is looped through and any failing item's index is used to delete the corresponding token from the database:

// For each message check if there was an error.
const tokensToRemove = [];
response.results.forEach((result, index) => {
  const error = result.error;
  if (error) {
    console.error('Failure sending notification to', tokens[index], error);
    // Cleanup the tokens who are not registered anymore.
    if (error.code === 'messaging/invalid-registration-token' ||
        error.code === 'messaging/registration-token-not-registered') {
      tokensToRemove.push(tokensSnapshot.ref.child(tokens[index]).remove());
    }
  }
});

You can find the source here.

The v1 HTTP protocol

The new "v1" HTTP protocol doesn't support multicast messages. Any message is sent to one device only, and sending a message to 5 devices requires 5 requests to the FCM server.

If a device token fails (error code UNREGISTERED), the token should be deleted. Again, your server needs to know which token the message was sent to. But in contrast to multicast messages it doesn't need to know in what order the tokens were provided, because there's only one token to consider.

Using Device Groups (legacy protocol only)

Device group messaging can relieve some of the burden of managing multiple devices. You should note that this feature is only supported by the legacy protocol and will likely never be supported by the new v1 protocol.

Device group messaging works like this: First, you create a "device group" for a specific user. Second, whenever you retrieve a token from the user's device, send it to your server and add it to the user's group. On logout, send the current token to your server and delete it from the group.

(Please refer to the official documentation on how to actually add and remove registration tokens to and from a device group. In short, you will use one of the Admin SDKs or the legacy HTTP API. Implementation details will depend on your programming language of choice.)

How is this easier than managing everything yourself? For one, you don't need to worry about duplicates because FCM will never add a token to the same group twice. And if a token fails after a message attempt, FCM will automatically remove it from the group.

Unfortunately, there's a potential downside. What if your user wants to delete his or her account and all associated data? In order to remove a device group, you first need to delete each and every registration token. Only after you've deleted them all will the group itself be removed from Firebase.

In other words, to completely remove a user's information from your system, including the device tokens, you need to have stored every device token in advance.

There are other disadvantages to using FCM's device group messaging, but those depend entirely on your use case. For example, you can't store metadata together with each token, like platform information, localization information etc.

(Note: If you use the FCM Admin SDKs, the actual HTTP requests used to send messages is hidden from you. The SDKs use both the legacy protocol and the v1 protocol, depending on the operation.)

Closing Remarks

Firebase is a great product with a lot of useful features. But I'd certainly welcome a better way to manage registration tokens using the new v1 protocol, which seems to be the future of FCM. At the moment, if you don't want to manage token groups yourself you may have to resort to third party services.

Also check out Handling Firebase Notification Messages in Your Web App