Keep Reading
If you enjoyed the post you just read, we have more to say!
A notification system is an essential feature for any modern-day application. At work, notifications improve our productivity by alerting us to comment mentions or upcoming meetings. Outside work, notifications alert us of important events like a friend's birthday or an upcoming flight.
In this series of articles, we'll design and implement a notification system from scratch. We'll do this in Ruby on Rails, but the concepts are widely applicable to any web application framework. For that reason, we'll try to use more generic concepts where we can. Part 1 (this article) focuses mostly on the database design.
As you will see, building a reliable, multi-tenant notification system is a lot of work. It's an excellent exercise for learning about real-time data communication, database design, and other concepts, but if you are thinking of building this as a feature in your product, we invite you to try MagicBell, a notification inbox you can add to your product in less than an hour!
With that out of the way, let's get started!
Before we dive in, let's write a few goals that describe the features our notification system must-have. We'd like our notification system to be:
An advanced notification system can do more but the goals above are sufficient to get started. In a future post, we can explore adding advanced features to our notification system like User Notification Preferences, Channel Specific Templates etc. You might also want to read our article on how to design a notification system.
Now that we've written down our goals, let's describe the entities that exist in our notification system and the attributes and relationships those entities will have
The Project entity represents a software application for which a notification is sent for. It can have the attributes
Name | Type | Description |
---|---|---|
id | UUID | A unique identifier to identify the project |
name | String | Name of the project |
and the relationships
Name | Type | Description |
---|---|---|
notifications | Has Many | Notifications sent for this project |
A Notification entity represents a notification to be sent. It can have the attributes
Name | Type | Description |
---|---|---|
id | UUID | A unique identifier to identify the notification |
title | Text | Title of the notification |
text | Text | Text of the notification |
on_click_url | String | The URL to redirect a user to when they click on the notification (in channels where clicking on a notification is feasible) |
sent_at | Time | The time at which the notification was sent |
and the relationships
Name | Type | Description |
---|---|---|
recipient_copies | Has Many | Copies of the notification sent to individual recipients |
A User entity represents an entity to whom a notification can be sent to. The User entity can have the attributes
Name | Type | Description |
---|---|---|
id | UUID | A unique identifier to identify a user |
first_name | String | First name of the user |
last_name | String | Last name of the user |
String | Email of the user |
A Notification Recipient Copy represents an instance/copy of a notification that is sent to a specific recipient. This entity is required as a Notification can be sent to many recipients in one API call. This entity can have the attributes
Name | Type | Description |
---|---|---|
id | UUID | A unique identifier to identify a notification recipient copy |
seen_at | Time | The time at the notification was first seen by the recipient |
read_at | Time | The time at the notification was first read by the recipient |
and the relationships
Name | Type | Description |
---|---|---|
recipient | Belongs To | The recipient to deliver the notification to |
notification | Belongs To | The notification whose copy this is |
We're now ready to pick the technologies to use to implement our notification system.
To store data, let's choose Postgres, a popular, open source, feature-rich and scalable relational database system. To implement the notification system, we will use Ruby and Rails as mentioned earlier.
For creating tables, we only need ActiveRecord, an opinionated and easy-to-use Object Relational Mapping (ORM) Framework, that is part of Rails. While it is easy to use ActiveRecord outside of Rails, it is less time-consuming to use all of Rails.
Even if you're unfamiliar with Rails, the code snippets below will still be very easy to understand as long as you're familiar with a web application framework in any language.
Install Rails
gem install rails
Create a Rails project. Let's name our Rails project "pigeon".
rails new pigeon
cd pigeon
Install bundler
gem install bundler
Install gems listed in the Gemfile
bundle install
Add ActiveRecord migrations to create the projects
, notifications
, users
and notification_recipients_copies
tables in Postgres
bundle exec rails g CreateProjects
class CreateProjects < ActiveRecord::Migration[5.1]
def change
create_table :projects do
t.string :name
t.timestamps
end
end
end
bundle exec rails g CreateNotifications
class CreateNotifications < ActiveRecord::Migration[5.1]
def change
create_table :notifications do
t.text :title
t.text :text
t.string :on_click_url
t.datetime :sent_at
t.timestamps
end
end
end
bundle exec rails g CreateUsers
class CreateUsers < ActiveRecord::Migrations[5.1]
def change
create_table :users do
t.string :email
t.timestamps
end
end
end
bundle exec rails g CreateNotificationRecipientCopies
class CreateNotificationRecipientCopies < ActiveRecord::Migrations[5.1]
def change
create_table :notification_recipient_copies do
t.references :recipient
t.references :email
t.timestamps
end
end
end
Add the Project
, Notification
, User
and the NotificationRecipientCopy
models
# In models/project.rb
class Project < ApplicationRecord
has_many :notifications
validates_presence_of :name
end
# In models/notification.rb
class Notification < ApplicationRecord
belongs_to :project
has_many :recipient_copies, class: "NotificationRecipientCopy"
validates_presence_of :title
validates_presence_of :text
validates_presence_of :recipient_emails
end
# In models/user.rb
class User < ApplicationRecord
has_many :recipient_copies, class: "NotificationRecipientCopy"
end
# In models/notification_recipient_copy.rb
class NotificationRecipientCopy < ApplicationRecord
belongs_to :user
belongs_to :recipient, :class => "User"
end
Now, modify the Notification
and NotificationRecipientCopy
models so they deliver a Notification
to recipients when one is created
# In app/models/notification.rb
class Notification < ApplicationRecord
# ...
before_create :associate_recipients
after_commit :deliver, :on => :create
attr_accessor :recipient_emails
def associate_recipients
recipient_emails.each do |recipient_email|
recipient = User.where(email: recipient_email).first
unless recipient
recipient = User.create(recipient_email: recipient_email).
end
recipients << recipient
end
end
def deliver
recipients.each do |recipient|
recipient_notification_copy = RecipientNotificationCopy.create(
notification: notification
recipient: recipient,
)
end
end
end
# In app/models/notification_recipient_copy.rb
require "lib/channels/email"
require "lib/channels/in_app_notification_center"
class NotificationRecipientCopy
# ...
after_create :deliver
def deliver
channels.each do |channel_class|
channel_class.new(self).deliver
end
end
private
def channels
[Channels::Email, Channels::InAppNotificationCenter]
end
end
We can use Sendgrid to deliver emails. They offer a Free plan. Signup for Sendgrid and obtain an API key. Configure Rails to deliver emails via Sendgrid
# Borrowed from https://sendgrid.com/docs/for-developers/sending-email/rubyonrails/
ActionMailer::Base.smtp_settings = {
:user_name => 'your_sendgrid_api_key',
:password => 'your_sendgrid_api_key',
:domain => 'pigeon.io',
:address => 'smtp.sendgrid.net',
:port => 587,
:authentication => :plain,
:enable_starttls_auto => true
}
and add the Email channel
class Channels
class Email
def initialize(notification_recipient_copy)
@notification_recipient_copy = notification_recipient_copy
end
def deliver
mail.deliver
end
def mail
Mail.new do
from "notifications@pigeon.io"
to @notification_recipient_copy.recipient.email
subject @notification_recipient_copy.title
body @notification_recipient_copy.text
end
end
end
end
To deliver notifications in real-time to your web application's JavaScript (which can render an in-app notification center), we can use a service like Ably. Like Sendgrid, Ably offers a Free plan as well.
Add the ably
gem to the Rails project's Gemfile
gem 'ably'
and install it
bundle install
Implement the InAppNotificationCenter
channel
class Channels
class InAppNotificationCenter
def initialize(notification_recipient_copy)
@notification_recipient_copy = notification_recipient_copy
end
def deliver
event_name = "new_notification"
event_data = {
event: {
name: event_name,
data: {
notification: {
id: notification_recipient_copy.id
}
}
}
}
ably.publish(event_name, event_data)
end
private
def ably_channel
"users/#{recipient.id}"
end
def ably
@ably ||= Ably::Rest.new(key: ENV['ABLY_PRIVATE_KEY'])
end
def recipient
notification_recipient_copy.recipient
end
end
end
That's it 😅! Let's try our notification system. Open a rails console
bundle exec rails console
and copy paste the code below and send a test notification to yourself
project = Project.create(name: "Test project")
project.notifications.create(
title: "First notification!",
text: "This is our first notification!",
recipient_emails: ["youremail@example.com"]
)
In a few minutes, Sendgrid should deliver you the email.
In the next article, we'll explore adding API endpoints to create notifications, fetch a user's notifications, mark a notification as read etc. Explore all MagicBell integrations and docs.
Thanks to Nisanth Chunduru for this blog post!
Related articles: