Skip to Content
Course content

217: Push Notifications from a Ruby Backend

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Push notifications are one of those features that seem simple on a diagram but can be a total headache in practice. You aren't just talking to a database; you're talking to a third-party gateway (like Firebase or Apple), which then talks to a device that might be offline or in airplane mode. For this lesson, we're going to build a "Price Drop Alert" system. When a product's price dips below a user's target, we'll fire off a notification.

Picking our gateway

I'm going to use Firebase Cloud Messaging (FCM) because it handles both Android and iOS, and it's the standard for most Ruby backends today. To keep this clean, we aren't going to use a massive wrapper gem that hides everything. Instead, we'll use faraday to make the HTTP requests. I prefer this because when the API changes—and they always do—you'll actually know how to debug the raw JSON request.

Structuring the notification payload

Before we touch the network, we need to figure out what we're sending. A push notification isn't just a string; it's a JSON object. We need a token (the unique ID of the user's device) and a notification object containing the title and body.

notification_payload = {
  message: {
    token: "user_device_token_abc123",
    notification: {
      title: "Price Drop Alert!",
      body: "Those sneakers you wanted are now $89.99!"
    },
    data: {
      product_id: "456",
      discount_type: "flash_sale"
    }
  }
}

Note that I added a data hash. This is crucial. The notification part is what the OS shows in the tray, but the data part is what your app uses to actually navigate the user to the right product page when they tap the notification.

The mistake: The "Legacy" Trap

Here is where I usually trip up when returning to an FCM project after a few months. I started writing the request using the old "Server Key" method—just adding a header like Authorization: key=AAA.... I hit the endpoint and got a 401 Unauthorized.

I realized I was trying to use the Legacy HTTP protocol. Google has moved to the FCM HTTP v1 API, which requires OAuth2 tokens. You can't just use a static string anymore; you need a Service Account JSON key and a short-lived Bearer token. This is a common point of frustration, but it's much more secure.

Handling OAuth2 and the Request

To fix this, I'll use the googleauth gem to handle the token exchange. It's a lifesaver because it manages the token expiration and refreshing for you. Here is how I'd wrap this into a reusable service object.

require 'faraday'
require 'googleauth'

class PushNotificationService
  FCM_URL = "https://fcm.googleapis.com/v1/projects/your-project-id/messages:send"

  def self.send_alert(device_token, title, body, extra_data = {})
    # Get the OAuth2 access token from the service account file
    auth = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: File.open('service-account.json'),
      scope: 'https://www.googleapis.com/auth/firebase.messaging'
    )
    auth.fetch_access_token!

    conn = Faraday.new
    response = conn.post(FCM_URL) do |req|
      req.headers['Authorization'] = "Bearer #{auth.access_token}"
      req.headers['Content-Type'] = 'application/json'
      req.body = {
        message: {
          token: device_token,
          notification: { title: title, body: body },
          data: extra_data
        }
      }.to_json
    end

    response.success?
  end
end

Wiring it into the business logic

Now we just need to call this when our price logic triggers. I wouldn't put this directly in the model—sending a network request during a database save is a recipe for a slow app. Instead, I'd wrap it in a background job. But for the sake of the example, here is the logic flow:

# Inside a PriceChecker service or Job
if product.current_price < user.target_price
  PushNotificationService.send_alert(
    user.fcm_token,
    "Price Drop!",
    "#{product.name} is now only #{product.current_price}!",
    { product_id: product.id }
  )
end

By separating the PushNotificationService from the logic that decides when to notify, you can easily swap FCM for another provider (like OneSignal or AWS SNS) without rewriting your price-checking logic.




📋 Practical Task

Build a "New Message" Notification Trigger

Imagine you are building a chat application. You have a Message model and a User model. The User model has a fcm_token attribute.

Your task is to implement a method within a new class called ChatNotificationService that sends a push notification to the recipient of a message. The notification should:

  • Include the sender's name in the title (e.g., "Message from Sarah").
  • Include a snippet of the message text in the body.
  • Pass the message_id in the data payload so the app can open the specific conversation.

Assume the googleauth and faraday gems are already configured and you have access to a service-account.json file. Write the class and the method that accepts a message object as its argument.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.