Implementing Push Notifications: The Back End

In the first part of this series we set up the front end with a Service Worker, a `manifest.json` file, and initialized Firebase. Now we need to create our database and watcher functions.

Article Series:

  1. Setting Up & Firebase
  2. The Back End (You are here)

Creating a Database

Log into Firebase and click on Database in the navigation. Under Data you can manually add database references and see changes happen in real-time.

Make sure to adjust the rule set under Rules so you don't have to fiddle with authentication during testing.

{
  "rules": {
    ".read": true,
    ".write": true
  }
}

Watching Database Changes with Cloud Functions

Remember the purpose of all this is to send a push notification whenever you publish a new blog post. So we need a way to watch for database changes in those data branches where the posts are being saved to.

With Firebase Cloud Functions we can automatically run backend code in response to events triggered by Firebase features.

Set up and initialize Firebase SDK for Cloud Functions

To start creating these functions we need to install the Firebase CLI. It requires Node v6.11.1 or later.

npm i firebase-tools -g

To initialize a project:

  1. Run firebase login
  2. Authenticate yourself
  3. Go to your project directory
  4. Run firebase init functions

A new folder called `functions` has been created. In there we have an `index.js` file in which we define our new functions.

Import the required Modules

We need to import the Cloud Functions and Admin SDK modules in `index.js` and initialize them.

const admin     = require('firebase-admin'),
      functions = require('firebase-function')

admin.initializeApp(functions.config().firebase)

The Firebase CLI will automatically install these dependencies. If you wish to add your own, modify the `package.json`, run npm install, and require them as you normally would.

Set up the Watcher

We target the database and create a reference we want to watch. In our case, we save to a posts branch which holds post IDs. Whenever a new post ID is added or deleted, we can react to that.

exports.sendPostNotification = functions.database.ref('/posts/{postID}').onWrite(event => {
  // react to changes    
}

The name of the export, sendPostNotification, is for distinguishing all your functions in the Firebase backend.

All other code examples will happen inside the onWrite function.

Check for Post Deletion

If a post is deleted, we probably shouldn't send a push notification. So we log a message and exit the function. The logs can be found in the Firebase Console under Functions → Logs.

First, we get the post ID and check if a title is present. If it is not, the post has been deleted.

const postID    = event.params.postID,
      postTitle = event.data.val()

if (!postTitle) return console.log(`Post ${postID} deleted.`)

Get Devices to show Notifications to

In the last article we saved a device token in the updateSubscriptionOnServer function to the database in a branch called device_ids. Now we need to retrieve these tokens to be able to send messages to them. We receive so called snapshots which are basically data references containing the token.

If no snapshot and therefore no device token could be retrieved, log a message and exit the function since we don't have anybody to send a push notification to.

const getDeviceTokensPromise = admin.database()
  .ref('device_ids')
  .once('value')
  .then(snapshots => {

      if (!snapshots) return console.log('No devices to send to.')

      // work with snapshots  
}

Create the Notification Message

If snapshots are available, we need to loop over them and run a function for each of them which finally sends the notification. But first, we need to populate it with a title, body, and an icon.

const payload = {
  notification: {
    title: `New Article: ${postTitle}`,
    body: 'Click to read article.',
    icon: 'https://mydomain.com/push-icon.png'
  }
}

snapshots.forEach(childSnapshot => {
  const token = childSnapshot.val()

  admin.messaging().sendToDevice(token, payload).then(response => {
    // handle response
  }
}

Handle Send Response

In case we fail to send or a token got invalid, we can remove it and log out a message.

response.results.forEach(result => {
  const error = result.error

  if (error) {
    console.error('Failed delivery to', token, error)

  if (error.code === 'messaging/invalid-registration-token' ||
      error.code === 'messaging/registration-token-not-registered') {

      childSnapshot.ref.remove()
      console.info('Was removed:', token)

  } else {
    console.info('Notification sent to', token)
  }

}

Deploy Firebase Functions

To upload your `index.js` to the cloud, we run the following command.

firebase deploy --only functions

Conclusion

Now when you add a new post, the subscribed users will receive a push notification to lead them back to your blog.

GitHub Repo Demo Site

Article Series:

  1. Setting Up & Firebase
  2. The Back End (You are here)