Post

Invalidating unused Firebase push tokens using scheduled functions

Invalidating unused Firebase push tokens using scheduled functions

Push messaging in modern mobile apps with Firebase is really common. The usual workflow is:

  • Integrating Firebase push SDKs / Cocoapods / SPM packages within the app.

  • Registering a project on the Firebase console.

  • On native apps, configuring the Firebase libraries, requesting for notification permissions, obtaining push tokens and saving them to a backend service.

Firebase functions to send push notifications

I recently wrote a serverless backend for one of my apps - one of the first things to setup was sending Push notifications to all my app users.

Firebase functions makes this a bit of a walk in the park.

Using the node.js FirebaseAdmin SDK, running something like sendToDevice(...) which does a multicast notification. More information can be found here.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//initialise Firebase Admin SDK.
const admin = require('firebase-admin');
admin.initializeApp();

//optional - if your firebase push tokens live in Firestore.
const db = admin.firestore();


//get tokens from Firestore.
const allTokens = await db.collection('tokens').get();

//run the sendToDevice call
admin.messaging().sendToDevice(allTokens, message, options)
            .then((response) => {
                console.log("response is ", response);
            });

Obtaining push tokens to persist in Firebase.

Again, Firebase functions to the rescue! Firebase functions support HTTP requests. More on this here.

Here’s an example again. Suppose we want the app to call a POST request on startup that sends through the push token generated by the Firebase SDK on Android or iOS:

1
2
3
4
5
6
7
8
9
10
11
exports.addPushTokenV2 = functions.https.onRequest(async (req, res) => {
    const token = req.body.data.token;

    if (!token) {
        return res.status(401);
    } else {
        //persist token to database or Firestore here.
        //return success
        return res.json({ result: `token with token: ${token} added` });
    }
});

Invalidating push tokens when no longer required.

A lot of apps obtain push tokens at login (an optimal way of doing this is by requesting the current token from Firebase’s local cache). Apps that support multiple users / login, usually also invalidate tokens on logout. But what if the user never logs out, but removes / uninstall the app? 🤔

The best way to invalidate these tokens, unfortunately is by looking for errors when a push message is sent outbound, like so:

1
2
3
4
5
6
7
8
9
10
admin.messaging().sendToDevice(tokens, message, options)
    .then((response) => {
        console.log('Successfully sent message:', response);
    }).catch((error) => {
        if (error.code === 'messaging/invalid-registration-token' ||
            error.code === 'messaging/registration-token-not-registered') {
            //a promise here to delete the token which can be pushed to an array,
            //then return promise.all 👍
        }
    });

This is however problematic in apps

  • Don’t want to send out bulk outbound notifications or
  • Only support async notifications ie. notifications that go out when the user performs an action.

There’s a better way. On closely inspecting the way Firebase functions sends out push notifications, I realised there is a

1
sendToDevice(registrationToken: string | string[], payload: MessagingPayload, options?: MessagingOptions): Promise<MessagingDevicesResponse>;

Here, MessagingOptions has a dryRun?: boolean variable, when set to true, it won’t actually send the push message but rather do a dry run.

Essentially, how we can create a scheduled Firebase function that

a) Polls for all push tokens

1
2
3
for (const [key, value] of tokens.entries()) {
        allTokens.push(key);
}

b) Sets dry run to true.

1
2
3
const options = {
  dryRun: true,
};

c) Finally, send out push notifications, poll for errors and remove invalid tokens:

1
2
3
4
5
6
7
8
9
10
11
12
admin
  .messaging()
  .sendToDevice(allTokens, message, options)
  .then((response) => {
    response.results.forEach((deviceResult, index) => {
      if (deviceResult.error) {
        let failedToken = allTokens[index];
        //push a promise to remove token from db.
      }
    });
  });
return Promise.all(tokensToRemove);

d) If you inspect the logs for your Firebase function

Fake token

It just works!

Nice one

This post is licensed under CC BY 4.0 by the author.