Push Notifications With MongoDB

Published

Push notifications are a staple of mobile and Internet of Things applications, and in this Write Stuff contribution Don Omondi, Founder and CTO of Campus Discounts, demonstrates how to leverage Compose MongoDB to send more effective push notifications.

Today’s technology has seen a sharp rise in connected devices, popularly known as the Internet of Things (IoT). Applications now live in watches, shoes and, perhaps rather oddly, in salt shakers too!

The IoT surge has also posed a few challenges for developers, one of them being how to send notifications to the plethora of connected devices. The main problem arises from the fact that different devices have different ways of subscribing to, receiving, and unsubscribing from notifications. We’ll see how to tackle this problem using MongoDB but first a little background information.

What are Push Notifications?

A push notification is a message that is "pushed" from a backend server or application to a user interface such as mobile applications and desktop applications.

A lot of developers make use of a notification service to send push notifications. A notification service provides a means to push notifications to many devices at once and may include other features such as delivery reports and analytics.

Push Notifications with Firebase Cloud Messaging

So with the preliminaries out of the way, let’s see how we can integrate Firebase Cloud Messaging (FCM), Google’s free notification service into a MongoDB powered backend. Through FCM we can send notifications to any service worker enabled browser (Chrome, Firefox, and Opera with Edge coming soon) as well as native Android and iOS applications.

To push with FCM, all we need to do is create an FCM app which will give us a server key. Thereafter, using either the Web, Android or iOS SDK generate an FCM client token once the user grants permissions (A practical example coming a bit later).

If you google around, you might be surprised to find that there are a number of people who’ve had a bit of trouble finding the GCM settings. You’ll have to click the settings icon/cog wheel next to your project name at the top of the Firebase console, then click on Project settings, and finally select the Cloud Messaging tab.

Armed with a server key and client token pair, sending a push notification is performed by a simple POST request to the FCM endpoint with an authorization header containing the key and a JSON encoded body of the notification with the client token in the "to" field like:

https://fcm.googleapis.com/fcm/send  
Content-Type: application/json  
Authorization: key=AIzaSyC...akjgSX0e4  
{
 "notification": {
 "title": "Message Title",
 "body": "Message body",
 "click_action" : "https://dummypage.com"
 },
 "to" : "eEz-Q2sG8nQ:APA91bHJQRT0JJ..."
}

The POST response will respond indicating whether the push notification was sent successfully or failed.

{
 "multicast_id": 7986976529786388478,
 "success": 1,
 "failure": 0,
 "canonical_ids": 0,
 "results": [{ 
    "message_id": "0:1496965028924567%e609af1cf9fd7ecd" 
  }]
}

That is really all it takes to send push notifications, but for many real life applications, it mustn’t stop there. It’s important to note that users don’t subscribe to push notifications but devices do, so we’ll have to find a way to link a client token to a user. This means saving the data in a store somewhere. We may also be interested in granting a user a subscription management interface as well as logging notifications. Let’s see why MongoDB is a good fit for this data store.

Some reasons to use MongoDB for Push Notifications

Storing Device Metadata: Many times you’d want to store some metadata about the device that has subscribed to push notifications, such as the browser vendor and version, or the toaster serial number or perhaps the salt shaker color. With nearly an infinite number of connectable devices, you’d really want a schemaless database for this.

Handling Shared Devices: A lot of people share devices, whether publicly like when using a cyber-café or privately when browsing on a friend's laptop, tablet or phone. They might not unsubscribe from notifications which the notification service provider will continue to happily deliver. We can mitigate this by setting a time to live (TTL) that automatically removes subscriptions that are not renewed within a given time. MongoDB has us covered here, too.

One User Many Devices: With the increase in connectable devices, it’s now common for one user to own many devices that use your application. For performance reasons, embedding a list of devices in one document per user would ensure maximum efficiency in many use cases.

Logging: You may also be interested in getting an overview of the recently pushed notifications. This can be useful for example to delete subscriptions that repeatedly fail to be delivered. MongoDB’s capped collection would be a perfect fit for this use case.

A Practical Example: Blog

Let’s say we have a blog and want to subscribe users to receive push notifications for example on new posts, comments or likes. Our blog will store each subscription in a MongoDB document using a sample schema below.

{
 "_id": ObjectId,
 "token": String,
 "subscribed_on": Date,
 "user_id": Integer,
 "fingerprint": String,
 "details": [
   "browser" : String,
   "os" : String,
   "osVersion" : String,
   "device" : String,
   "deviceType" : String,
   "deviceVendor" : String,
   "cpu" : String
 ]
}

We already know we need to store two fields in the FCM, client token and the user_id. We'll also want to know the time a user subscribed to receive a push, which we'll store in the subscribed_on field.

Furthermore, to help a user manage their subscriptions, we’ll need to store a device’s information like the operating system and version, browser vendor, and others. This way you can help a user associate a FCM notification endpoint to a device. We created a details array field to store such arbitrary data.

28th June, 2017 via Chrome on Android 5.1  

Finally, let’s assume we also want to reduce the number of duplicate subscriptions, which can happen when people share devices or when the notification service generates a new subscription Universally Unique Identifier (UUID) for the same device. Duplicate data is bad because it can lead to different notifications being sent to the same device but for different users. So we’ll need to create a field to store a value that can fairly accurately identify a device, to achieve this, we’ll use a technique called device fingerprinting.

A device fingerprint, also sometimes called a machine fingerprint or browser fingerprint is information collected about a remote computing device for the purpose of identification. Fingerprints can be used to fully or partially identify individual users or devices even when cookies are turned off.

With the document schema ready, we’ll need to create a MongoDB collection to hold them, let’s create one called ‘push_notifications’ from mongo shell

> db.createCollection(‘push_notifications’)

From MongoDB 3.2 and beyond we can enforce some level of strict schema by using document validation. You can read more about it as well as find some examples in Document Validation in MongoDB By Example. In our example, we want to ensure that every subscription document has a non-null subscribed_on field with a data type Date. We also need a non-null device fingerprint value of type string and a non-null user_id value of type Int. Let’s enforce it with this validation.

> db.createCollection( "push_notifications",
 {
   validator: { 
     $and: [
       { token: { $type: "string" } },
       { token: { $exists: true } },
       { subscribed_on: { $type: "date" } },
       { subscribed_on: { $exists: true } },
       { user_id: { $type: "int" } },
       { user_id: { $exists: true } },
       { fingerprint: { $type: "string" } },
       { fingerprint: { $exists: true } }
     ]
   }
 }
)

For speedy lookups on documents matching certain client tokens, let’s create an index on the token field. We can also declare this index as unique so as to prevent duplicate subscriptions from different users using the same token.

db.push_notifications.createIndex( { "token": 1 }, { unique: true} )

Since we wish to have subscriptions automatically removed after a certain amount of time, let’s create a TTL index to tell MongoDB to delete documents after some time.

db.push_notifications.createIndex( { "subscribed_on": 1 }, { expireAfterSeconds: 604800 } )  

This will purge documents whose subscribed_on field’s value is greater than or equal to 1 week from the time MongoDB runs its background checks. To keep active subscriptions, update the subscribed_on field periodically, for example, once a day, so that they are always less than 1 week old.

To enable us to quickly look up all subscriptions for a specific user, let’s create an index on the user_id field.

db.push_notifications.createIndex({ user_id: 1 })  

If your app allows non-logged in users to subscribe to push, then you can make this a partial index from the MongoDB shell as follows.

db.push_notifications.createIndex({ user_id: 1 } , { partialFilterExpression: { user_id: { $exists: true } } })  

Also, don’t forget to remove the user_id requirements from the document validation. For MongoDB versions prior to 3.2, use sparse indexes instead.

That’s it, with the database schema all set up, our backend is ready to push. It’s now up to our frontend to send information so our backend can know to whom. For that, we’ll use some JavaScript. The procedure is to first ask the user for permissions, then register the device for push notifications and pass its UUID, fingerprint as well as some details to our backend.

Setting Up the JS Client

For getting the browser fingerprint on the client side, we can use the conveniently named library clientjs. clientjs also allows us to get device specific information such as the OS, OS version, CPU type, Device Type and more which we’ll use to fill our details array.

<script src="/path/to/client.min.js"></script>  
<script src="https://www.gstatic.com/firebasejs/4.1.2/firebase-app.js"></script>  
<script src="https://www.gstatic.com/firebasejs/4.1.2/firebasemessaging.js"></script>  
<script type="text/javascript">  
 // Create a new ClientJS object
 var client = new ClientJS();
 // Get the client's fingerprint id
 var fingerprint = client.getFingerprint();
 // Get the browser's details that interest us
 var details = {
   browser: client.getBrowser(),
   os: client.getOS(),
   osVersion: client.getOSVersion(),
   device: client.getDevice(),
   deviceType: client.getDeviceType(),
   deviceVendor: client.getDeviceVendor(),
   cpu: client.getCPU()
 };
 // Initialize Firebase
 var config = {
   apiKey: "<YOUR_FCM_APP_API_KEY>",
   messagingSenderId: "<YOUR_FCM_APP_PROJECT_NUMBER>"
 };
 firebase.initializeApp(config);
 const messaging = firebase.messaging();
 messaging.requestPermission()
   .then(function() {
     console.log('Notification permission granted.');
     // Get Instance ID token. Initially this makes a network call, once retrieved
     // subsequent calls to getToken will return from cache.
     messaging.getToken().then(function(currentToken) {
       if (currentToken)  {
         sendSubscriptionToServer(currentToken , fingerprint , details);
       } else {
           console.log('No Instance ID token available. Request permission to generate one.');
       }
    })
   .catch(function(err) {
     console.log('An error occurred while retrieving token. ', err);
   });
 }).catch(function(err) {
   console.log('Unable to get permission to notify.', err);
 });

 // Handle incoming messages. Called when:
 // - a message is received while the app has focus
 // - the user clicks on an app notification created by a sevice worker
 // `messaging.setBackgroundMessageHandler` handler.
 messaging.onMessage(function(payload) {
   console.log("Message received. ", payload);
   showNotification(payload.data);
 });

 // Callback fired if Instance ID token is updated.
 messaging.onTokenRefresh(function() {
   pushSubscribe();
 });

 // Callback fired if Instance ID token is updated.
 messaging.onTokenRefresh(function() {
   messaging.getToken().then(function(refreshedToken) {
     console.log('Token refreshed.');
     sendSubscriptionToServer(refreshedToken , fingerprint , details);
   }).catch(function(err) {
     console.log('Unable to retrieve refreshed token ', err);
   });
 });
 sendSubscriptionToServer(token , fingerprint , details) {
   let formData = new FormData();
   formData.append('token' , token);
   formData.append('fingerprint' , fingerprint);
   formData.append('details' , JSON.stringify(details));
   $.ajax({
     url: '/api/notifications/subscribe',
     type: "POST",
     data: formData,
     dataType: 'json',
     contentType: false,
     processData: false
   });
 }
</script>  

We’ll also need to create a small service worker script called firebase-messagingsw.js which is also responsible for enabling background pushes.

// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/4.1.2/firebase-app.js');  
importScripts('https://www.gstatic.com/firebasejs/4.1.2/firebase-messaging.js');  
// Initialize the Firebase app in the service worker by passing in the
// messagingSenderId.
firebase.initializeApp({  
 'messagingSenderId': <YOUR_FCM_APP_PROJECT_NUMBER>'
});
// Retrieve an instance of Firebase Messaging so that it can handle background
// messages.
const messaging = firebase.messaging();  
messaging.setBackgroundMessageHandler(function(payload) {  
 console.log('[firebase-messaging-sw.js] Received background message ', payload);
 // Customize notification here
 const notificationTitle = 'Background Message Title';
 const notificationOptions = {
 body: 'Background Message body.',
 icon: '/firebase-logo.png'
 };
 return self.registration.showNotification(notificationTitle,
 notificationOptions);
});

Wrapping Up

So that’s about it then. If you are yet to implement push notifications for your app(s), hopefully now you know how simple it is. Just adapt the sample code in this article, grab yourself a MongoDB instance and push away!


Do you want to shed light on a favorite feature in your preferred database? Why not write about it for Write Stuff?

Don Omondi is a full-stack developer and the Founder and CTO of [Campus Discounts](https://campus-discounts.com/). Besides the typical coffee and code, he also loves old school music over a game of chess or checkers.

attribution Kate SERBIN

This article is licensed with CC-BY-NC-SA 4.0 by Compose.

Conquer the Data Layer

Spend your time developing apps, not managing databases.