Skip to main content

Pluggable Channels

Notification channels are pluggable adapters. The platform defines an interface (NotificationChannel) and ships two built-in implementations (WebSocket, Browser Push). Every other channel — email, SMS, Telegram, Slack, Discord, Webhook — is installed as a module that registers an adapter conforming to this interface.

This architecture lets the platform support any delivery channel without modifying kernel code.


NotificationChannel Interface

Every channel adapter must implement six components:

ComponentTypeDescription
identifierstringUnique, lowercase slug. Used as the channel field in send(). Examples: email, sms, telegram. Cannot contain spaces or uppercase.
namestringHuman-readable display name shown in Settings → Notifications. Example: "Email (SendGrid)".
iconstringURL to the channel icon (32×32 px PNG or SVG). Shown in Settings and notification history.
setupComponentReact componentUI rendered in Settings → Notifications → Connect. Typically contains API key inputs, webhook URL fields, and a "Verify" button.
send(recipient, message) => Promise<void>The delivery method. Called by the Notify Service worker for each notification. May throw on failure — the worker handles retry.
validate(config) => Promise<boolean>Called when the admin saves provider credentials. Returns true if the configuration is valid (e.g. SendGrid API key is accepted). Used to detect misconfiguration before the first real send attempt.

Go Interface (Server-Side)

The Notify Service calls adapters via the Go interface:

// services/notify/internal/channels/channel.go
type NotificationChannel interface {
Identifier() string
Name() string
IconURL() string

// Validate credentials before first use
Validate(ctx context.Context, config ChannelConfig) error

// Deliver a notification
Send(ctx context.Context, recipient Recipient, msg Message) error

// UI component served to the frontend for setup
SetupComponentPath() string
}

type Recipient struct {
UserID string
TenantID string
Target string // email address, phone number, chat ID, webhook URL, etc.
}

type Message struct {
Subject string
Body string
Priority string
Metadata map[string]any
}

Declaring a Channel in the Manifest

An adapter module declares its channel in module.manifest.json:

{
"name": "@acme/notify-sendgrid",
"version": "1.0.0",
"notificationChannels": [
{
"identifier": "email",
"name": "Email (SendGrid)",
"icon": "https://cdn.acme.com/icons/sendgrid-32.png",
"setupComponent": "./src/components/SendGridSetup.tsx",
"sendHandler": "SendGridChannel"
}
]
}

When the admin installs this adapter module, the Notify Service automatically loads the SendGridChannel handler and registers it under the email identifier. The channel immediately appears in Settings → Notifications.


Register a Channel via REST

Channel adapters are registered programmatically when the module installs:

POST https://api.septemcore.com/v1/notifications/channels
Authorization: Bearer <access_token>
Content-Type: application/json

{
"identifier": "email",
"name": "Email (SendGrid)",
"icon": "https://cdn.acme.com/icons/sendgrid-32.png",
"config": {
"apiKey": "SG.xxxxxxxxxxxxxxxxxxxxxxxx",
"fromAddress": "[email protected]",
"fromName": "Acme Platform"
}
}

Response 201 Created:

{
"channelId": "01j9pachx100000000000000",
"identifier": "email",
"name": "Email (SendGrid)",
"status": "verified",
"registeredAt": "2026-04-15T10:30:00.000Z"
}

The status field reflects the result of the validate() call:

StatusMeaning
verifiedvalidate() passed — credentials are valid
unverifiedvalidate() skipped (offline registration)
invalidvalidate() failed — check config fields

SDK — registerChannel() and listChannels()

// Register a channel adapter programmatically
await kernel.notify().registerChannel({
identifier: 'telegram',
name: 'Telegram',
icon: 'https://cdn.acme.com/icons/telegram-32.png',
config: {
botToken: '7123456789:AAHxxxxxxxx',
chatId: '-100123456789',
},
});

// List all registered channels for the current tenant
const channels = await kernel.notify().listChannels();
// [
// { identifier: 'websocket', name: 'WebSocket', status: 'built-in' },
// { identifier: 'push', name: 'Browser Push', status: 'built-in' },
// { identifier: 'email', name: 'Email (SendGrid)', status: 'verified' },
// { identifier: 'telegram', name: 'Telegram', status: 'verified' },
// ]

List Channels via REST

GET https://api.septemcore.com/v1/notifications/channels
Authorization: Bearer <access_token>
{
"data": [
{
"identifier": "websocket",
"name": "WebSocket",
"icon": null,
"status": "built-in"
},
{
"identifier": "push",
"name": "Browser Push",
"icon": null,
"status": "built-in"
},
{
"identifier": "email",
"name": "Email (SendGrid)",
"icon": "https://cdn.acme.com/icons/sendgrid-32.png",
"status": "verified",
"registeredAt": "2026-04-15T10:30:00.000Z"
}
]
}

Channel Availability in send()

The channel field in send() must match the identifier of a registered channel. An unregistered channel returns 400 Bad Request:

{
"type": "https://api.septemcore.com/problems/validation-error",
"status": 400,
"detail": "Channel 'whatsapp' is not registered for this tenant.",
"code": "CHANNEL_NOT_FOUND"
}

Built-in channels (websocket, push) are always available without registration.


Implementing a Custom Adapter

To add a channel not in the standard list (e.g. a proprietary internal messaging system):

  1. Create a module that implements NotificationChannel in Go.
  2. Declare it in module.manifest.json under notificationChannels.
  3. Install the module in the tenant.
  4. The channel is immediately available in send({ channel: 'my-channel' }).

The Notify Service never has hard-coded knowledge of specific providers. Adding or removing a provider is purely a module install/uninstall operation — no kernel changes required.


Error Reference

ScenarioHTTPCode
Channel identifier not registered400CHANNEL_NOT_FOUND
Channel validate() failed on register422CHANNEL_CONFIG_INVALID
Duplicate identifier for this tenant409CHANNEL_ALREADY_EXISTS
identifier contains uppercase or spaces400validation-error