One of the most common features for native mobile app development is push notifications. As we ready our Android application for release here at PLM, I've had the job of implementing push notification support on the server, i.e., getting real time notifications from our web application to our users' android devices. For this task, We are using Firebase Cloud Messenger, Google's service for sending push notifications to iOS devices, Android devices, and web browsers.

Here, I want to briefly walk through that process and introduce you to a tool we developed and open-sourced along the way, the firebase_cloud_messenger gem. My goal is to get you up and running from your Ruby web application quickly and easily.

Why Write a New Gem?

Google just recently released a brand new version of their HTTP Firebase Cloud Messenger API, and gave legacy status to their older HTTP and XMPP APIs. The new API uses OAuth instead of server key auth for greater security, and allows better support for all the different kinds of push notifications you might want to send, be it iOS APN, Android, or Webpush notifications. Google does not supply an SDK for Ruby, and we did not find any other libraries that supported the new API. After we wrote our own into our main web application, we wanted to share it with the community! PLM 💛s OSS!

Creating A Firebase Cloud Messenger Project

I found that Google's docs on how to get all set up were pretty good.

Here's a couple things that weren't obvious for getting the web server set up.

  1. When you set up a Firebase project, it will create a Serice Account that contains all the credentials necessary to auth up your push notification requests to FCM server. On the "Service Account" tab. there's a "Generate New Private Key" button. Clicking it will download a json file containing the service account information, including a private key necessary to sign a jwt that's used to fetch the OAuth bearer token that then authorizes FCM requests (Don't worry--the gem will do all this for you). Keep it safe--you can only download it once. That's got to get onto the server that will be making push notifications requests to FCM (or at least it's values loaded into env vars).
  2. In order to make API requests to FCM, you may need to enable the FCM API from Google's API Console. This tripped me up for a while, because it's not something mentioned in the docs. An error message from FCM led me to almost the right place, but it took a fair amount of clicking around to figure out what the problem was.

Once the service account credentials file is in the right place, the API is enabled, and the Firebase project is set up in your Android (or iOS or web) app, you're ready to start pushing out notifications.

Registering Device Tokens

In order to send notifiations out to a user's device, you'll need to store their registration token somewhere accessible to your service or application. We chose to implement an endpoint that maps a user to a device registration token the version of the application they are on.

Keep in mind in here that, while registration tokens are relatively stable, they can change if a user reinstalls the app or clears their app data (and probably a couple of other scenarios, too). A user might also be associated with multiple tokens if they use your application on multiple devices. We created a AndroidDeviceToken ActiveRecord model that is associated with our User model and allows users to be mapped to multiple registration tokens.

Also keep in mind that there could be multiple different users using the application on the same device. What if one person logs out and another logs in? The device still only has one registration token, and now it must be remapped to the new user, or else that user will not receive push notifications. We set up remapping like this: Whenever a new user logs in, the Android app hits the endpoint responsible for recording the user-token mapping. If the token is already present, it reassociates it with the new user. If it's not, a new mapping record is created. That looks something like this:

device_token = AndroidDeviceToken.find_or_initialize_by(token: params[:token])
device_token.user_id = params[:user_id]
device_token.build_version = params[:build_version]
device_token.save!

StackOverflow has some other suggestions about how to handle some related scenarios that I find helpful.

Sending Push Notifications

Once your project is set up and you're successfully registering tokens, you're ready to start sending out push notifications.

In your Gemfile, include firebase_cloud_messenger. You'll need to tell it where your credenitals file lives, which you can do either by setting the GOOGLE_APPLICATION_CREDENTIALS env var to the path where the json file is stored, or by calling FirebaseCloudMessenger.credentials_path = "path/to/credentials/file.json". We do the latter, through a Rails initializer. The gem will take care of reading off those values to figure out what URL to make FCM requests to, and to fetch the OAuth token from Google's auth servers.

The rest is easy! Messages can be sent to individual users' devices by grabbing their registration tokens, building a message, and instructing firebase_cloud_messenger to go ahead and push it out. You can check out Google's spec for FCM requests here. Each json object represented there has its own corresponding data class in firebase_cloud_messenger, with properties that can be set through a hash arg to the initializer or through writer methods, but messages can be built as hashes as well. You can read more about messages, including instructions on how to validate messages prior to sending them to FCM in the gem docs, but sending a message will look something like this:

android_notification = {
  notification: {
    title: "Title text",
    body: "Body text"
  }
}
message = FirebaseCloudMessenger::Message.new(android: android_notification,
                                              token: "some_registration_token")
FirebaseCloudMessenger.send(message: message)

The gem will take care of the rest, including fetching a fresh OAuth token if necessary. Successful requests will return a hash of information from FCM, and unsuccessful requests will throw an error that is_a? FirebaseCloudMessenger::Error for easy rescuing and logging. I also found that Google API Console provided a decent overview of traffic to FCM, letting me know whether FCM was successfully receiving and responding to my requests.

Having Trouble?

If the gem isn't working as you expected or doesn't do something you'd like, we'd love to hear about it! File a Github issue or submit a PR!.

Here's hoping this guide and our gem helps you easily and effectively reach out to your native app users!