This article has been partly inspired by an upcoming chapter of the author's Advanced CableReady book, and tailored to fit this guest post for AppSignal.
Notifications are a typical cross-cutting concern shared by many web applications.
The Noticed gem makes developing notifications fantastically easy by providing a database-backed model and pluggable delivery methods for your Ruby on Rails application. It comes with built-in support for mailers, websockets, and a couple of other delivery methods.
We'll also examine the merits of using the CableReady gem for triggering system notifications in your Ruby on Rails application.
Let's get into it!
Prerequisites and Requirements
Every now and then, you might need to trigger system notifications from your app. You can achieve this with the Notifications API.
For example, let's say your application allows users to upload large files which take a long time to be transcoded. You may want to send users a notification once their video upload completes. This means they can switch to a new task in the meantime and don't have to keep your application open for several minutes.
Luckily, it's easy to implement and flesh out system notifications with these two prerequisites:
- Noticed has support for custom delivery methods (Noticed exposes a simple API to implement any transport mechanism you like — for example, posting to Discord servers).
- CableReady has a notification operation arrow in its quiver.
The list of requirements for CableReady and Noticed is short — you simply need the following:
- an ActionCable server running
- an ActiveJob backend (typically used by Noticed)
Note: The entire sample code for this project is available on GitHub. You can also follow along below — we will guide you step-by-step.
A CableReady Primer for your Ruby on Rails Application
Let's start by making ourselves familiar with CableReady. Released in 2017, it can be thought of as the "missing ActionCable standard library".
Before the advent of Turbo, the only way to craft real-time applications in Ruby on Rails was through ActionCable. ActionCable is the native Rails wrapper around WebSockets, providing both a server-side and client-side API to send messages through a persistent connection in both directions at any time.
The downside to this approach was (and is) that you have to write a lot of boilerplate code to make it happen.
This is where CableReady helps by providing an abstraction layer around a multitude of DOM operations to be triggered on the server. A few examples include:
- DOM mutations (
inner_html
,insert_adjacent_html
,morph
, etc.) - DOM element property mutations (
add_css_class
,remove_css_class
,set_dataset_property
, etc.) - dispatching arbitrary DOM events
- browser history manipulations
- notifications (which we will make use of)
The CableReady Server and Client
How does CableReady work its magic? In a nutshell, it consists of a server-side and client-side part.
On the server side, the module CableReady::Broadcaster
can be included in any part of your application that calls for it. This could be in a job, a model callback, or a plain controller. But first, an ActionCable channel has to be in place. To cite CableReady's official documentation:
Note that visitors
is called a stream identifier. It can be used to target either a broad audience or only specific clients subscribed to your channel. To conclude the example, we can include the broadcaster module in a model and send a console.log
to the client after sign-up:
On the client side, the logic is simple. Create a subscription to the aforementioned channel. Then, in its received hook, call CableReady.perform
on all operations passed over the wire:
CableReady vs. Turbo for Rails
Summing up, when should you use CableReady, and when should you avoid it?
With the introduction of Turbo, the web development community received a powerful toolbox to craft server-rendered reactive applications. Being essentially a frontend technology with powerful server-side bindings for Rails, it fits well into the standard Model-View-Controller (MVC) stack. Thus, it can cover most of your typical app's requirements.
CableReady, on the other hand, is the Swiss army knife of real-time Rails development and should be used with care. It is a powerful abstraction that can seem very inviting to use pervasively. But if you imagine that every part of your DOM can be mutated from any location in your app, you'll understand that this can lead to race conditions and hard-to-track-down bugs.
There are cases like the one at hand, though, where CableReady makes perfect sense because it allows for more fine-grained control over the DOM.
Asked for a simple TLDR, I would respond that Turbo is for application developers, while CableReady is for library builders. But as we will see, there are gray areas between the two.
Noticed — Simple Notifications for Ruby on Rails Applications
The second library we will apply to deliver system notifications is Chris Oliver's Noticed gem. At its heart, it is built upon an ActiveRecord model that models a single notification to a recipient. It holds common metadata, such as:
- who a notification was sent to (the recipient)
- when the notification was read
- any parameters the notification is associated with (typically a reference to another model)
If you are familiar with how the ActiveStorage/ActionText meta-tables work, this is very similar.
Adjacent to this, Noticed employs POROs (Plain Old Ruby Objects, i.e., objects without any connection to Rails or other frameworks), which serve as blueprints for actual notifications. These are, somewhat misleadingly, also called Notifications and carry logic about how to render and distribute them. Here is an example from the README:
We will see this at work shortly. Of special interest are the deliver_by
invocations, as they determine which delivery methods this notification should use:
deliver_by :database
stores a Notification record (of the model mentioned above) for later accessdeliver_by :action_cable
sends it via a defined ActionCable channel and stream (defaultNoticed::NotificationChannel
)deliver_by :email
specifies a mailer used to send the notification. The example displays how to factor in any preferences the recipient(s) might have set.
Our goal for the remainder of this article is to implement a custom delivery method that will send our system notifications.
A Custom Delivery Method: System Notifications via the Notifications API
Before we set out to do this, let's clear the ground by creating a new Rails application. Because integrating with CableReady is easier this way, I opted for the esbuild
JavaScript option over importmaps
:
Note: At the time of writing, the current Rails version is 7.0.4. If you have an existing Rails application, you can skip the next step, but make sure you have a User model or similar concept to act as a notification recipient.
1. Prepare Recipients
Noticed needs a User model to act as recipients, so to be concise, pull in Devise and generate a User model.
Afterward, open the Rails console and create a sample user:
2. Add Noticed
Next, let's add Noticed to our bundle and generate the database model.
We do as told, run db:migrate
and add the polymorphic has_many
association to our User model:
The final piece before we can put this to the test is to create a blueprint PORO:
We tweak it a bit so that it just uses the database delivery method and a placeholder message for the moment. You would typically add required params here, like a model id, to construct the message and the URL to link to — see the Noticed README for details.
Using only the Rails console, we can demonstrate how to deliver a notification based on this PORO now:
As we can see, Noticed performs a database insert, so now we can grab all Notifications
for a specified user:
This is enough for us to construct a simple index view that lists all delivered notifications for the current user:
After spinning up the app with bin/dev
, we can log in and browse to http://localhost:3000/notifications:
3. Installing CableReady
To make use of CableReady, we need to install it. Luckily, this is quickly done:
Let's generate a NotificationChannel
to deliver our messages:
This will add all the missing ActionCable (JavaScript) dependencies and scaffold the respective channel files, specifically app/channels/notification_channel.rb
and app/javascript/channels/notification_channel.js
.
For the server-side channel, we inherit from Noticed::NotificationChannel
:
Before we continue, we need to ensure our ActionCable is authenticated for Devise users. I won't go into details about that here. The necessary boilerplate looks like this:
Check out the StimulusReflex docs for other options.
On the client side, we need to add the tiny bit of setup code mentioned above:
Note: Redis is required for ActionCable to work, so the rest of this article assumes you have a local server running.
4. Delivery Method Implementation
To broadcast system notifications, let's generate a new delivery method:
The scaffolded class looks like this:
Let's continue by drafting how we envision our deliver
method to work.
We have borrowed the channel
method in part from the built-in ActionCable delivery method. It allows us to pass in a channel via a class method option. Otherwise, it falls back to the provided Noticed::NotificationsChannel
.
Then we use CableReady's notification
method to broadcast the respective instance to the recipient.
To put it into action, we have to connect it to our notification PORO:
5. Putting It to Work
Now all that's left to do is try it out. We can simply run this again from the Rails console:
Assuming you are still logged in, the browser will first ask you for permission to receive notifications on behalf of the app:
Once you have confirmed this, you get this beautiful pop-up notification from your browser:
Wrapping Up
Taking a fast lane tour through CableReady and Noticed, we have demonstrated how to integrate a native browser API into your app. The result is a simple, coherent way to deliver system notifications to your users.
This use case is also meant to illustrate how easy it is to mix CableReady into your use case. If you think ahead one step, decoupling the drafted delivery method into a library is not hard.
I hope this inspires you to look for manifestations of vertical reactive problem domains in your app and give CableReady a try.
Read more by picking up my new book Advanced CableReady.
Use the coupon code APPSIGNAL-PROMO and save $10!
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!