User notifications with Rails, Noticed, and Hotwire
Tags: ruby, rails, tutorial, webdev
Published: 2022-03-22 02:36:31

A nearly-universal need in web applications is user notifications. An event happens in the application that the user cares about, and you inform the user of the event. A common example is in applications that have a commenting system &mdash; when a user mentions another user in a comment, the application notifies the mentioned user of that comment through email.

The channel(s) used to notify users of new important events depends on the application but the path very often includes an in-app notification widget that shows the user their latest notifications. Other common options include emails, text messages, Slack, and Discord.

Rails developers that need to add a notification system to their application often turn to [Noticed](https://github.com/excid3/noticed). Noticed is a gem that makes it easy to add new, multi-channel notifications to Rails applications.

Today, we are going to take a look at how Noticed works by using Noticed to implement in-app user notifications. We will send those notifications to logged in users in real-time with [Turbo Streams](https://turbo.hotwired.dev/reference/streams) and, for extra fun, we will load user notifications inside of a [Turbo Frame](https://turbo.hotwired.dev/reference/frames).

When we are finished, our application will work like this:

![A screen recording of two web browsers open side by side. In one, a user fills out a message into a web form and submits it. In the other, the message the user wrote appears under a Notifications heading after the form is submittd](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s5eet623ucrkvnlt8z6i.gif)

Before we begin, this tutorial assumes that you are comfortable building simple Ruby on Rails applications independently. No prior knowledge of Turbo or Noticed is required.

Let’s dive in!

## Application setup
To follow along with this tutorial, start by cloning [this repository from Github](https://github.com/DavidColby/user-notices) and then set it up:

```bash
cd user-notices
bin/setup
```

The starter repo contains a Rails 7 application with Turbo, Tailwind, and Devise ready to go. The starter repo uses Ruby `3.0.2`, but everything in this tutorial will work fine with Ruby `2.7` and `3.1`, if you prefer.

If you want to work from your own application instead of cloning the starter, you will need a Rails 7 application with Turbo installed and an authentication system built around a `User` model.

When you’re ready to start building, start the server and build Tailwind’s css with `bin/dev`.

## Noticed setup
Our starter application comes with a Devise-powered user model and the root path set to the `Dashboard#show` action which just contains links to sign in or sign out for now. Before diving in to the code, create at least one user through the form at [http://localhost:3000/users/sign_up](http://localhost:3000/users/sign_up) so that you can login and test notifications later in this tutorial.

Eventually, our users will be able to create `Messages` for other users in the application. Each time a message is a created, a new `Notification` will be created, and the user the message is for will see that notification on their dashboard.

Before any of that can happen, we need to add Noticed to our application and scaffold a Message resource. Start by adding Noticed, from your terminal:

```bash
bundle add noticed
rails generate noticed:model
rails db:migrate
```

These commands are straight from the Noticed [installation docs](https://github.com/excid3/noticed#-installation). If your Rails app is running, be sure to restart it after adding the Noticed gem to your Gemfile with `bundle add`.

Next, update `app/models/user.rb` to associate notifications with users:

```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :notifications, as: :recipient
end
```

Here, we added the `notifications` `has_many` association, as directed by the Noticed setup script.

Now our users can receive notifications, but we don’t have anything useful to notify them about. We will fix that by scaffolding a `Message` resource. From your terminal:

```bash
rails g scaffold message content:text user:references
rails db:migrate
```

Thanks to the magic of Rails, the scaffold generator gives us almost everything we need to start creating messages and associating them with users. Because we are using Tailwind via the [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) gem, the scaffold generator also includes some nice looking base styles too.

After the generator runs, update the `User` model again at `app/models/user.rb`:

```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :messages
  has_many :notifications, as: :recipient
end
```

Here, we added `has_many :messages` to set up the other side of the messages relationship.

For convenience, we can also add a link to the messages index page in `app/views/dashboard/show.html.erb`:

```erb
<div>
  <% if user_signed_in? %>
    Signed in as <%= current_user.email %>. <%= button_to "Sign out", destroy_user_session_path, method: :delete, class: "text-blue-500" %>
    <%= link_to "All messages", messages_path, class: "text-blue-500" %>
  <% else %>
    <%= link_to "Sign in", new_user_session_path, class: "text-blue-500" %>
  <% end %>
</div>
```

And then make a small adjustment to the messages form so we don’t have to memorize user ids:

```erb
<%= form_with(model: message, class: "contents") do |form| %>
  <% if message.errors.any? %>
    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
      <h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2>

      <ul>
        <% message.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :content %>
    <%= form.text_area :content, rows: 4, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

  <div class="my-5">
    <%= form.label :user_id %>
    <%= form.select :user_id, options_for_select(User.all.pluck(:email, :id)), class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
  </div>

  <div class="inline">
    <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
  </div>
<% end %>
```

With these changes in place, head to [http://localhost:3000/messages](http://localhost:3000/messages) and create a couple of messages to ensure that creating messages works as expected.

Now that we have Noticed installed and messages ready to go, next up we will send users notifications when a new message is created.

## Message notifications
Our goal in this section is to create and display notifications to logged in users on the `Dashboard#show` page.

Step one is to add a new notification, using the the generator built-in to Noticed. From your terminal:

```bash
rails generate noticed:notification MessageNotification
```

This generator creates a new `MessageNotification` class at `app/notifications/message_notification.rb`. Head there next and make a few small updates:

```ruby
class MessageNotification < Noticed::Base
  deliver_by :database

  param :message

  def message
    params[:message].content
  end
  
  def url
    message_path(params[:message])
  end
end
```

Here, `deliver_by :database` stores the newly created notification in the database, which will be important for Turbo Stream broadcasts later. If we wanted to send the notification by email too, we could add `deliver_by :email, mailer: SomeMailer`, as described in the Noticed [docs](https://github.com/excid3/noticed/blob/master/docs/delivery_methods/email.md).

`param :message` serializes the `message` object and stores it with the notification record in the database. We use that serialized `message` in the `message` and `url` methods. Serializing objects into params in this manner makes it easy to access records related to the notification without managing complex references &mdash; just dump the record(s) you need into `params` and profit.

We can now use our `MessageNotification` to send notifications to users when a new message is created. Next we need to put the `MessageNotification` class into use. Head to `app/models/message.rb` and update it:

```ruby
class Message < ApplicationRecord
  has_noticed_notifications

  belongs_to :user

  after_create_commit :notify_user

  def notify_user
    MessageNotification.with(message: self).deliver_later(user)
  end
end
```

Each time a message is created (`after_create_commit`), `notify_user` runs and creates a new `MessageNotification`, serializing the `message` object and delivering the message to the message’s user. We also add [has_noticed_notifications](https://github.com/excid3/noticed#associating-notifications) to ensure that when a message is destroyed, any related notifications are destroyed too.

With those small changes, we now have a database-backed notification system up and running. Neat!

We have notifications in the database now but there is no way for users to see those notifications anywhere, which is not very useful. Next up, we will create a `Notifications` controller to display notifications to users.

From your terminal, generate the controller and a partial to render each notification:

```bash
rails g controller Notifications index
touch app/views/notifications/_notification.html.erb
```

Head to `config/routes.rb` and add a notifications path helper:

```ruby
Rails.application.routes.draw do
  resources :notifications, only: [:index]
  resources :messages
  devise_for :users
  get 'dashboard/show'
  root "dashboard#show"
end
```

Update the `NotificationsController` at `app/controllers/notifications_controller.rb`:

```ruby
class NotificationsController < ApplicationController
  def index
    @notifications = Notification.where(recipient: current_user)
  end
end
```

Here, we scope `notifications` to the `current_user`, so users only see their own notifications when logged in to the application.

Update the new Notifications index view at `app/views/notifications/index.html.erb`:

```erb
<%= turbo_frame_tag "notifications" do %>
  <h1 class="font-bold text-4xl">Notifications</h1>
  <ul>
    <%= render @notifications %>
  </ul>
<% end %>
```

Note the `turbo_frame_tag` wrapping the list of notifications. Our plan is to render the list of notifications on the dashboard show page &mdash; we will use Turbo Frame’s [eager-loading functionality](https://turbo.hotwired.dev/reference/frames#eager-loaded-frame) to load the content of the notifications index page on the dashboard show page.

`render @notifications` relies on Rails’ [collection rendering](https://guides.rubyonrails.org/v7.0/layouts_and_rendering.html#rendering-collections) to render each notification. Before this will work, we need to fill in `app/views/notifications/_notification.html.erb`:

```erb
<li>
  <div>
    <p class="text-gray-700">
      <%= notification.to_notification.message %>
    </p>
    <div class="flex justify-between mt-1 text-gray-500 text-sm space-x-4">
      <p>
        Received on <%= notification.created_at.to_date %>
      </p>
      <p>
        Status: <%= notification.read? ? "Read" : "Unread" %>
      </p>
    </div>
  </div>
</li>
```

Here, we use `to_notification` from Noticed to access the `message` method we added in `MessageNotification` earlier, and use the built-in `read?` method from Noticed to check if the user has read the notification or not.

One last step here before users can see notifications on the dashboard. Head to `app/views/dashboard/show.html.erb` and update it to add an eager-loaded Turbo Frame for logged in users:

```erb
<div class="flex justify-between">
  <% if user_signed_in? %>
    <div>
      Signed in as <%= current_user.email %>. <%= button_to "Sign out", destroy_user_session_path, method: :delete, class: "text-blue-500" %>
      <%= link_to "All messages", messages_path, class: "text-blue-500" %>
    </div>
    <%= turbo_frame_tag "notifications", src: notifications_path %>
  <% else %>
    <%= link_to "Sign in", new_user_session_path, class: "text-blue-500" %>
  <% end %>
</div>
```

The `turbo_frame_tag` has an id of `notifications`, which matches the Turbo Frame rendered by `Notifications#index`. When Turbo eager-loads content for a Turbo Frame, it expects the url passed to `src` to return a response that includes a Turbo Frame with a matching id.

The sequence of events for our eager-loaded notifications index page is this:

* A logged in user visits `Dashboard#show`
* `Dashboard#show` is loaded
* Turbo sees the `turbo_frame_tag` with an `src` attribute and initiates a new request to `/notifications`
* `Notifications#index` returns HTML that includes a `turbo_frame_tag` that matches the tag that initiated the request
* Turbo extracts the contents of the `turbo_frame_tag` and uses that content to replace the content of the existing Turbo Frame

At this point, logged in users can see their notifications on the dashboard. Test it out by logging in as a user and then creating a new messages for that user. Refresh the dashboard as that user and see that your notifications are listed on the dashboard:

![A screenshot of a web page open to a list of notifications under a Notifications heading](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pn9lgvu6bx3u289qmf84.png)

While our users can see notifications, they don’t see those notifications in real-time &mdash; they have to manually refresh the dashboard before they see new notifications. Let’s wrap up this tutorial by making notifications real-time with Turbo Stream broadcasts.

## Real-time notifications with Turbo Streams
Turbo model broadcasts, powered by [turbo-rails](https://github.com/hotwired/turbo-rails), make it easy to send real-time updates to users with ActionCable. When we finish this section, each time a new notification is created, a Turbo Stream broadcast will be sent over an ActionCable channel that will automatically insert new notifications for a user into the user's dashboard notifications list.

To start, update `app/models/notification.rb` to trigger a Turbo Stream broadcast when a new notification is created:

```ruby
class Notification < ApplicationRecord
  include Noticed::Model
  belongs_to :recipient, polymorphic: true

  after_create_commit :broadcast_to_recipient

  def broadcast_to_recipient
    broadcast_append_later_to(
      recipient,
      :notifications,
      target: 'notifications-list',
      partial: 'notifications/notification',
      locals: {
        notification: self
      }
    )
  end
end
```

Here, the `broadcast_to_recipient` method `appends` a new notification to the list of notifications. To ensure that only the user the notification is for receives the broadcast, we set the broadcast channel to `recipient, :notifications`, as described in the `turbo-rails` [source](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb#L22).

The model update handles sending Turbo Stream broadcasts, but before the broadcast will be picked up by the front end we need to subscribe users to a Turbo Stream channel. We also must ensure the markup includes a `notifications-list` id (matching the `target` passed to `broadcast_append_later_to`) for the Turbo Stream to update.

Starting in `app/views/notifications/index.html.erb`, add the `notifications-list` id to the `ul` containing the list of notifications:

```erb
<%= turbo_frame_tag "notifications" do %>
  <h1 class="font-bold text-4xl">Notifications</h1>
  <ul id="notifications-list">
    <%= render @notifications %>
  </ul>
<% end %>
```

And then in `app/views/dashboard/show.html.erb`:

```erb
<div class="flex justify-between">
  <% if user_signed_in? %>
    <div>
      Signed in as <%= current_user.email %>. <%= button_to "Sign out", destroy_user_session_path, method: :delete, class: "text-blue-500" %>
      <%= link_to "All messages", messages_path, class: "text-blue-500" %>
    </div>
    <%= turbo_stream_from current_user, :notifications %>
    <%= turbo_frame_tag "notifications", src: notifications_path %>
  <% else %>
    <%= link_to "Sign in", new_user_session_path, class: "text-blue-500" %>
  <% end %>
</div>
```

Here, the `turbo_stream_from` helper from turbo-rails subscribes the user to a channel that matches the channel our model is broadcasting from. `current_user, :notifications` in the view == `recipient, :notifications` in the model).

When a user visits the dashboard, the `turbo_stream_from` helper opens an ActionCable subscription to the matching channel. Broadcasts to that channel are picked up by Turbo and used to update the page when new broadcasts are received. The scoping of the channel to the `current_user` ensures that users do not receive broadcasts intended for another user so that our new message notifications are only sent to the user the message is for.

With these changes in place, login as a user in one browser and head to the [dashboard](http://localhost:3000). Confirm the ActionCable channel subscription is created by checking the server logs for a line that looks like this:

```
Turbo::StreamsChannel is streaming from Z2lkOi8vdXNlci1ub3RpY2VzL1VzZXIvMQ:notifications
```

In another browser, head to the [new messages form](http://localhost:3000/messages/new) and create a message, setting the user on the form to the user that you are logged in as in the first browser. If all has gone well, the new notification will be added to the list instantly, with no page updates required.

![A screen recording of two web browsers open side by side. In one, a user fills out a message into a web form and submits it. In the other, the message the user wrote appears under a Notifications heading after the form is submittd.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rpav5m99bty6z79vtffa.gif)

Great work following along with this tutorial, that is all of the code for the day!

## Wrapping up & further reading
Today we built a simple notification system with Rails and Noticed, and we used Turbo to display new notifications for users in real-time. Noticed is an extremely powerful gem that makes the work of building and expanding a multi-channel notification system in a Rails app much simpler, and the easy integration with Turbo Streams makes it a great match for any modern Rails application.

In our tutorial application, we displayed notifications as a static list of every notification the user has ever received, with no way to interact with them or remove them from the list. We also required users to be on their dashboard to see the new notification.

In a production application, we might expand our Notifications controller to include an `update` method that allows users to mark notifications as read and clear those notifications from the list.

We would would also likely build a notification indicator in the main navigation that is rendered on every page of our application with an icon indicating unread notifications (think the ubiquitous bell icon seen across thousands of web applications).

The neat thing about this tutorial’s approach is that the basic approach remains the same even for a more sophisticated implementation. Use Turbo Streams to broadcast new notifications to users and update the UI in real-time. Use built-in Noticed methods to act on notifications (like `mark_as_read!` to read a notification). Use a Turbo Frame to fetch notifications for a user and load those notifications into a section of a larger page.

For further learning on Noticed, Turbo Streams, and Turbo Frames:
* Check out the [GoRails tutorial](https://gorails.com/episodes/rails-notifications-with-noticed) on Noticed (Chris from GoRails wrote Noticed, thanks Chris!)
* Read the simple but effective Noticed docs, starting in the [repo’s readme](https://github.com/excid3/noticed) and digging in to specific [delivery methods](https://github.com/excid3/noticed/tree/master/docs/delivery_methods) as needed
* Solidify Turbo concepts with my [Turbo 101 article](https://www.colby.so/posts/turbo-rails-101-todo-list), and my [Turbo Frames](https://www.colby.so/posts/turbo-frames-on-rails) and [Turbo Streams](https://www.colby.so/posts/turbo-streams-on-rails) on Rails articles, for more on how to use Turbo with Rails

Finally, if you want to dig deeper into implementing notifications in a Rails application, a chapter of [my book](https://davidcolby.gumroad.com/l/hotwired-ats) is dedicated to building a very light notification system from scratch, with inspiration from Noticed. In the book, we add real-time updates in a more realistic way, with the standard bell icon, pop-out notification list, and the ability to mark notifications as read with a click. The book is written in this same, step-by-step tutorial style, and covers building a modern Rails application from scratch with StimulusReflex, CableReady, Hotwire, and friends.

Building your own notification system is a great exercise if you want to use Noticed in a real production application later, since you will have a much greater understanding of, and appreciation for, what Noticed offers you.

That’s all for today. As always, thanks for reading!