How I Built a Scalable Notification System for Serversinc
June 24, 2025 · Product Updates
One of the most important parts of managing infrastructure is knowing when things happen; when a new server is provisioned, when a deployment fails, or when that one buggy app starts eating up too many resources.
In this article, I’ll walk you through how I built the Notification System for Serversinc; making sure alerts arrive quickly, reliably, and in a human-readable way, regardless of the channel.
The Goals
At the planning stage, I kept a few key priorities in mind:
- Deliver notifications quickly: There’s no point sending a “System Alert” ten minutes after the server’s gone down. Users need to know instantly.
- Make it easily extendable: Initially, we needed Slack, Webhook, and Discord alerts, but the design had to support future services too without requiring huge refactors.
The Chosen Solution
Each Workspace can create a Notification Channel for every service where alerts should be sent; Slack, Email, Webhook, or any other supported method.
Here’s an example of a channel:
{
"name": "Slack",
"type": "slack",
"identifier": "https://webhooks.slack.com/1234",
"notifications": "server.created,server.updated,server.deleted"
}
This schema kept things simple, yet flexible:
- name and type are self‑explanatory.
- identifier is the Slack Webhook URL (or an email address if the
type
isemail
). - notifications is a comma‑separated list of events in the format
resource.action
orresource.action.status
(e.g.,server.created
orapplication.deployment.failed
).
Why this approach?
I chose this design because it gives users complete flexibility over where they want to receive notifications whilst maintaining an easy-to-extend pattern. In the future, this approach will also support channel filtering, making it possible to direct certain alerts to specific destinations. For example, production‑only alerts can go exclusively to the #production
channel in Slack.
Dispatching Notifications to the Queue
With the Notification Channels configured, I built a SendChannelNotification
job. Its role is to:
- Receive a
notification_type
, a Workspace, and some context (such as aServer
or anApplication
). - Dispatch the alert to every channel configured for that
notification_type
.
Here’s the flow:
- Generate the Notification Content based on the
notification_type
. - Query All Channels in the Workspace that have registered this
notification_type
. - Send the Notification via the channel handler.
- Record the Result (and any failures) in a
ChannelHistory
table.
Generating Notification Content
The job receives a notification_type
such as server.created
which isn’t very human‑readable. So, it maps it to a structured, friendly message:
For example, server.created
becomes:
- Title:
Server Created
- Content:
A new server has been provisioned.
- Color: Bright green (success)
- Emoji: ✨ (just for a bit of flair)
Here’s an example:
protected function getContentForType(NotificationType $notificationType): string
{
return match ($notificationType) {
NotificationType::ServerCreated => 'A new server has been created.',
NotificationType::ServerUpdated => 'A server has been updated.',
// Add more cases as needed
default => "Notification: {$notificationType->value}",
};
}
💡 Side Note: All notification types are defined in an Enum (
NotificationType
), ensuring only valid types can be used. This avoids errors caused by typos (e.g.,appication.created
vs.application.created
).
enum NotificationType: string
{
case ServerCreated = 'server.created';
case ServerUpdated = 'server.updated';
// Add more here in the future
}
Including Links in Slack and Discord Messages
Slack and Discord both support linkable messages via their Webhook APIs:
- Slack uses
blocks
andactions
(with alink_button
) - Discord uses
embeds
that support URLs
To enable this, I extract the id from the $context
array passed to the Job and build a url
pointing to the relevant resource. This link is added automatically when generating Slack or Discord alerts, making it easy for Users to click straight through to the relevant page.
Building the URL is easy with Laravel;
route('servers.view', [id => $this->context['id']])
discord
Slack
Finding All Channels
A simple LIKE
query finds any channel that has subscribed to the specific notification_type
:
NotificationChannel::query()
->whereLike('notifications', '%' . $this->notificationType . '%')
Sending Alerts to Each Channel
Each channel triggers the processChannel()
method. This method:
- Determines the channel’s
type
(Slack, Discord, etc.). - Dispatches the alert via a
sendNotification()
method that matches the channeltype
to its handler:- Slack channels call
$this->sendSlack()
- Discord channels call
$this->sendDiscord()
- Others can be added easily as needed
- Slack channels call
- Constructs the request body formatted for the target service (including any links).
- Sends the request and captures the response.
- Extracts any errors.
- Records everything in the
ChannelHistory
table.
Saving the History
One issue I’ve run into with many Webhook integrations is that platforms don’t record the “paper trail”, making it hard to debug when something fails.
To fix this, every notification is stored for 30 days in a ChannelHistory
table:
{
"channel_id": "123",
"data": "[JSON_ARRAY]",
"notification_type": "server.created",
"response": "text field",
"error": "text field",
"success": true
}
This doesn’t just help with debugging; it acts like a pseudo-audit log in a way, making it easy for Users to trace when an action happened and where alerts were sent across their Workspace.
Final Thoughts
I’m really proud of this design; it feels completely flexible and easy to extend as new services or requirements come along. So far, it’s worked like a treat, especially as I’ve been dogfooding Serversinc myself. In fact, it’s a big part of how I use the platform every day.