← Back to blog

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 is email).
  • notifications is a comma‑separated list of events in the format resource.action or resource.action.status (e.g., server.created or application.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:

  1. Receive a notification_type, a Workspace, and some context (such as a Server or an Application).
  2. 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
}

Slack and Discord both support linkable messages via their Webhook APIs:

  • Slack uses blocks and actions (with a link_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 channel type to its handler:
    • Slack channels call $this->sendSlack()
    • Discord channels call $this->sendDiscord()
    • Others can be added easily as needed
  • 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.