Managing Cloud Messaging Tokens

If you’re using Firebase Cloud Messaging (also known as FCM), you might know that it requires registering tokens for each device that you want to send a message to. Registration tokens are important to implement correctly for ensuring accurate message delivery data reported in the Firebase console or exported to BigQuery.

Firebase’s FCM documentation describes best practices for registration token management, and today, I’m going to cover a concrete code example of how to actually implement generating, retrieving, storing, and updating registration tokens using Cloud Firestore and Cloud Functions for Firebase. If you’re already using FCM and have your own servers, you’ll see comments in the code snippets below for when and how you should be interacting with your servers.

Generating Registration Tokens

On initial startup of your app, the FCM SDK generates a registration token for the client app instance. You use this token when sending messages to your app via FCM APIs, although you don’t need to manage these tokens at all if you only use them to subscribe to FCM topics.

Acquiring this token could be beneficial for combining it with your own business logic to send a specific message to a specific app user (such as a dinner’s-ready notification). To access this token, you’ll extend FirebaseMessagingService and override onNewToken(), which will fire whenever a new token is generated.

There are two scenarios when onNewToken is called:

When a new token is generated after initial app startup

Whenever an existing token is changed.

There are scenarios when the existing token is changed:

  • The app is restored on a new device
  • The user uninstalls/reinstall the app
  • The user clears app data
  • Certain device hardware failures can result in new tokens being created as a mechanism to work around the failures
  • FCM may also force tokens to change by calling this method as part of their operations. An example of this is: if the token encryption key is compromised, FCM rolls out new tokens to all app instances.
override fun onNewToken(token: String) {
  // Manage FCM token
}

Retrieving Registration Tokens

To get the registration token, use Firebase.messaging.token, and upon completion, retrieve the token:

val token = Firebase.messaging.token.await()

Calling getToken() every month is recommended to mitigate any potential device hardware failures.

Storing Registration Tokens

After either generating a new registration token or getting an existing one, your server should track the timing of each client’s token update so as to ensure freshness of these tokens. This way, you know messages sent from the FCM API are targeting registered devices that are likely to actually be available. To do so, record the timestamp at which the token is received. This blog post will later discuss how the timestamp helps you manage registration tokens through app instance lifecycles.

As an example, to store registration tokens in this blog post, I’ll use Firebase Firestore, a no-SQL database. If you’re using your own backend server, the code snippets shown will have a comment saying what you should do at that point with your own server.

Going forward with the Firestore example, create a fcmTokens collection, and each document ID will be the user ID. You can use Firebase Authentication or your own server to manage your users to get the user ID. Inside the document, there will be two fields:

  • Registration token
  • Timestamp at which the token was last updated

Using Firestore’s set function, store the fields into the fcmTokens collection, and as a reminder, set will create a new document if one with the existing key (user ID in this case) is not present. Otherwise, it will update the existing document, which is the correct behavior here.

fun storeToken(token: String) {
  // If you're running your own server, call API to send token and today's date for the user
  
  // Example shown below with Firestore
  // Add token and timestamp to Firestore for this user
  val device_token = hashMapOf(
    "token" to token,
    "timestamp" to FieldValue.serverTimestamp(),
  )

  // Get user ID from Firebase Auth or your own server
  Firebase.firestore.collection("fcmTokens").document(user.uid)
          .set(device_token).await()
}

When you get a new token as well as when you retrieve one, store it into Firestore by calling storeToken, which is shown above:

override fun onNewToken(token: String) {
  // Manage FCM token
  storeToken(token)
}

suspend fun getAndStoreRegToken(): String {
  val token = Firebase.messaging.token.await()
  storeToken(token)
  return token
}

Every time the user opens the app, you should get the token from FCM (call Firebase.messaging.token) to ensure the device has the right token, then update the timestamp and optionally the token if it has changed.

To know whether the token has changed, you can cache the token in Android’s Shared Preferences so that every time the device opens, the cached token gets compared and it can get updated to the server accordingly if needed.

The getAndStoreRegToken() function needs to get changed accordingly:

fun getAndStoreRegToken() {
    val preferences = this.getPreferences(Context.MODE_PRIVATE)
    val tokenStored = preferences.getString("deviceToken", "")

    lifecycleScope.launch {
        val token = Firebase.messaging.token.await()
        if (tokenStored == "" || tokenStored != token) {
            storeToken()
        }
    }
}

Invalid Token Responses

When sending a message, you may receive an error response indicating an invalid token. In this case, you should delete it from your system. With the HTTP v1 API, these error messages may indicate that your send request targeted stale or invalid tokens:

  • UNREGISTERED (HTTP 404)
  • INVALID_ARGUMENT (HTTP 400)

Note that, because INVALID_ARGUMENT is thrown also in cases of issues in the message payload, it signals an invalid token only if the payload is completely valid. See ErrorCodes for more information.

If you are certain that the message payload is valid and you receive either of these responses for a targeted token, it is safe to delete your record of this token, since it will never again be valid. As the message would be sent from a server environment, the following code could be put in a Cloud Functions for Firebase function to remove the device token from Firestore:

// Registration token comes from the client FCM SDKs
const registrationToken = 'YOUR_REGISTRATION_TOKEN';

const message = {
  data: {
    // Information you want to send inside of notification
  },
  token: registrationToken
};

// Send message to device with provided registration token
getMessaging().send(message)
  .then((response) => {
    // Response is a message ID string.
  })
  .catch((error) => {
    // Delete token for user if error code is UNREGISTERED or INVALID_ARGUMENT
    if (errorCode == "messaging/invalid-argument" 
        || errorCode == "messaging/registration-token-not-registered") {
      // If you're running your own server, call API to delete the token for the user
        
      // Example shown below with Firestore
      // Get user ID from Firebase Auth or your own server
      Firebase.firestore.collection("fcmTokens").document(user.uid).delete()
  });

Updating Registration Tokens

To ensure that a device’s registration token is fresh, you should periodically retrieve and update all existing registration tokens. To do so, you’ll need to add app logic in the client app to retrieve the current token, and then send the token along with a timestamp to the server to store. This could be a monthly job.

Using WorkManager

In Android, there’s WorkManager, which handles schedulable persistent work, and you can use this to update the registration token on the server every month. In an UpdateTokenWorker class that extends CoroutineWorker, you can get the token and send it to your own server or save it to Firestore in this blog post’s running example.

class UpdateTokenWorker(appContext: Context, workerParams: WorkerParameters):
   CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        // Refresh the token and send it to your server
        var token = Firebase.messaging.token.await()
        storeToken()

        // Indicate whether the work finished successfully with the Result
        return Result.success()
    }
}

In your main activity’s onCreate(), build a PeriodicWorkRequest and enqueue it:

override fun onCreate(savedInstanceState: Bundle?) {
   // … other code in onCreate()
   // Refresh token and send to server every month
   val saveRequest =
      PeriodicWorkRequestBuilder<UpdateTokenWorker>(730, TimeUnit.HOURS)
          .build()

   WorkManager.getInstance(this).enqueueUniquePeriodicWork(
      "saveRequest", ExistingPeriodicWorkPolicy.UPDATE, saveRequest);
}

You can decide how often you’d like to refresh your registration tokens, but do make sure to update tokens periodically and adopt a threshold for when you consider tokens stale; an update frequency of once per month likely strikes a good balance between battery impact vs. detecting inactive registration tokens, and there is no benefit to doing the refresh more frequently than weekly. By doing this refresh, you ensure that any device which goes inactive will refresh its registration when it becomes active again.

This is also a good time to refresh your topic subscriptions by subscribing the client to relevant topics again if you are using that functionality.

Remove Stale Tokens

So far, we’ve talked about getting and updating registration tokens, and we’ve stored into Firestore the device token and a timestamp. How will you use that information?

Well, before you send a message to a device, you can ensure that the timestamp is within your staleness window period (as a reminder, we recommend two months). You can remove stale tokens based on the timestamp, and you can do this every day via a Cloud Function (example below) or on your own server.

const EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 60; // 60 days
exports.pruneTokens = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
  // Get all documents where the timestamp exceeds is not within the past month
  const staleTokensResult = await admin.firestore().collection('fcmTokens')
      .where("timestamp", "<", Date.now() - EXPIRATION_TIME)
      .get();

  // Delete devices with stale tokens
  staleTokensResult.forEach(function(doc) { doc.ref.delete(); });
});

Conclusion

So there you have it! This is an example of how to manage your FCM tokens using best practices as described in the documentation. When you periodically update your registration tokens, you can be fairly confident that the device is active and so when you send a message to that device, the delivery is successful. This can help with your delivery success, which then can help with engaging your users!