UtilityGenius News

How To: Developing a Notifications System

Written by Kyle Plump | Jun 27, 2024

Introduction

The modern web is rarely static.  Users expect responsiveness and timely communication of events and information when interacting with your products.  At UtilityGenius, we’re driven by the ability to provide accurate, up to date information about utility rebate programs.  If providing value through data is at the core of our products, a question we’ve asked ourselves is how can we tailor this data directly to the user and provide it as quickly as possible?   

Our solution is instantaneous data delivery with a real-time notification system.  There are a few technical challenges to expect when developing a real-time notification system: it needs to be efficient, reliable, and work with your existing tech stack.  Here’s how we achieved this at UtilityGenius.

Setup / Tech Stack

We use a traditional architecture of having separate front-end and back-end applications.  In this example, we will set up real-time notifications using:

  • Ruby on Rails as the back-end application framework.
  • Action Cable to integrate Websockets.
  • React and Next.js as the front-end application.

Back-end Setup

Let’s start with the back-end setup.  The most conventional way to provide real-time events is via a websocket connection, so that’s what we’ll use here.  Luckily, Ruby on Rails is a ‘batteries included’ framework, which means it already provides a system for working with websockets, called Action Cable.

We’ll spare you the finer detail of every aspect of Action Cable or websockets here, but you can check out the full docs here.  Conceptually, it’s broken down into three parts:

  • Connections
  • Channels
  • Broadcasting

Creating the connection

First, we’ll create a connection.  This is exactly what it sounds like: the link between the front-end and the back-end, via a websocket protocol. In Rails, the default connection is found at: app/channels/application_cable/connection.rb

 

NOTE: you will likely need to add the following line to the bottom of your routes.rb file if it does not exist:

As you can see in the above code snippet, the Action Cable connection is primarily used for creating a connection and identifying the user who has connected. In this example, we define that the connected user is identified by :current_user, and on connect we find a UtilitygeniusUser model to assign to the current_user.  This current_user class variable will then be accessible to the current class, as well as being available in our channels (but more on this later). 

This was a bit of a contrived example, as the current_user will always be the UtilitygeniusUser with id=1.  Let’s fill in this function with a more realistic implementation. 

To authenticate users on our separate front-end application, we are using the devise gem in conjunction with devise-jwt.  To identify the user who is attempting to open a websocket connection, the front-end application will send its verified JWT along with the connection request.  Our find_verified_user private function can then interface directly with Warden (the core of devise) to decode the JWT and find the associated UtilitygeniusUser.  The updated code now looks like this:

With that, we now have the ability to open websocket connections between our applications, along with associating the connection with a UtilitygeniusUser.  Let’s move on to the second piece of this puzzle: creating a channel.

Creating a channel

A channel “encapsulates a logical unit of work, similar to what a controller does in a typical MVC setup” (See this overview for more info: https://guides.rubyonrails.org/action_cable_overview.html#terminology-channels).  Simply, a channel is a stream of events filtered to a certain topic, that a consumer can ‘subscribe’ to via the channel’s name.  For example, we can have an ArticlesChannel, which will broadcast any newly published articles to all of the channel's consumers.  A channel can have any number of consumers (or subscribers).  In our example, we are creating a real-time notification system, so let’s set up a notifications channel called UtilityGeniusNotificationChannel

As previously mentioned, the :current_user is available in our channels.  Unlike the ArticlesChannel example, notifications need to be scoped to a specific user.  To do this, we can use the :current_user ID in our channel stream, basically instances of the channel that we’ll send data through.  While the channel name will be the same for every consumer, each subscription (when a consumer subscribes to a channel) will be unique based on the connecting user.  As long as the client application passes a valid JWT with its connection request, we’ve essentially created a private stream, scoped to a particular user.  

Broadcasting events

Now that we have a channel to stream events through, it’s easy to send messages to the consumer using Action Cable’s built in broadcast function.   The broadcast function takes two parameters: a stream name and data to send across the stream.  In the context of UtilityGenius user notifications, one instance where a user would want to be notified is when a member of their team has updated a project to include additional lighting fixtures, changing the rebate estimate, energy savings, and ROI for the retrofit.

While this isn’t a real implementation of the described use case, it serves as an example of the usage of the broadcast function that can be used in any other situation.  In this example, we can see that as a UtilityGenius project is saved, we send a hash with a message and project ID across the stream scoped to the currently logged in UtilitygeniusUser. Now, we have a complete back-end setup for creating websocket connections, and sending data to the consumer in real-time over channels.  We can move on to creating the connection consumer in the front-end application.

Front-end Setup

Our front-end application is using React and the Next.js metaframework, as well as Auth.js (formerly Next-Auth) as our authentication solution.  Our plan of attack here is to create a custom React hook called useNotifications that we can include in any part of our application where we want to receive up to date notification information.

Creating the consumer

The first step is installing the Action Cable node package.  This allows us to create websocket consumers in the client as easily as we did on the back-end. 

Let’s create our useNotifications hook and create the consumer: 

So far in the hook, all we’ve done is create a basic consumer.  You will notice the createConsumer takes in the URL of the back-end connection. (NOTE: this URL is prefixed with ws:// since it is a websocket connection, rather than traditional HTTP).  Once we have our consumer, we can create a new subscription to the channel UtilityGeniusNotificationChannel, and a basic received function, which will run once the consumer receives a new message.

NOTE: you might get any number of errors at this step.  This was the one I received:

Next.js provides the ability to server render some of your code.  The Javascript Action Cable package is client-only.  If you get stuck by an error at this step, try to move your consumer setup code into a function, and then call the function once the window object has been initialized.

The current implementation is not yet complete. Remember during the back-end connection setup how we identified the connecting user via a JWT?  Let’s go ahead and get the JWT from the Auth.js session, and pass it along with the connection request.  The updated code now looks like this:

Now, we can actually return some data from this hook, including some derived state: the number of unseen notifications (this is useful in our implementation, as we can pop up a number in the navigation bar once a new notification comes through).

And finally, use this hook into a component:

We are now receiving real-time notifications! 

There is a bug though.  You notice, as websocket messages come through, the number of unseen notifications state value is not increasing past 1.  Why is this?  This bug is an issue in Javascript, and not Action Cable specific, so I won’t spend too much time here.  It boils down to us having a ‘stale closure’ in our code.  You can read more about the issue here: https://stackoverflow.com/questions/62806541/how-to-solve-the-react-hook-closure-issue

The simplest solution to fix our issue is to simply pass the state setter function the new state in callback form:

With that, we have a full real-time notifications system!

Conclusion

Creating a notification system allows for increased touch points across your application. It can increase customer stickiness through personalized interactions and live time updates on relevant changes. Our email notification system allows users to receive weekly notifications on program change updates. Now, these notifications can be received in the platform, as well as alerts when a team member edits a Building or Project, or when new features are available. Expansions of this feature might include notifying users of new content being published on the site.

If you’re interested in setting up a notification system in your own application, we hope this has been a handy guide for getting started. Oh, and if you’re already a UtilityGenius user? Make sure to check out your account to see this example in action!