Notifications
Nimbus notifications deliver messages across multiple channels — email and realtime broadcast — from a single, unified interface. Implement the notification.Notification interface, then call notification.Send() to dispatch on all channels.
The Notification Interface
Every notification implements two methods — return nil or an empty channel to skip a channel:
import "github.com/CodeSyncr/nimbus/notification"
type Notification interface {
// ToMail returns the mail message, or nil to skip email.
ToMail() *mail.Message
// ToBroadcast returns a channel name and payload for realtime delivery.
// Return empty channel string to skip broadcast.
ToBroadcast() (channel string, payload any)
}
Creating a Notification
package notifications
import (
"fmt"
"github.com/CodeSyncr/nimbus/mail"
)
type OrderShipped struct {
OrderID uint
UserEmail string
UserName string
}
func (n *OrderShipped) ToMail() *mail.Message {
return &mail.Message{
To: []string{n.UserEmail},
Subject: fmt.Sprintf("Order #%d Shipped!", n.OrderID),
HTML: fmt.Sprintf("<h1>Hi %s</h1><p>Your order #%d has shipped.</p>", n.UserName, n.OrderID),
}
}
func (n *OrderShipped) ToBroadcast() (string, any) {
return fmt.Sprintf("users/%s/orders", n.UserEmail), map[string]any{
"event": "order.shipped",
"order_id": n.OrderID,
}
}
Sending Notifications
notification.Send() dispatches on all channels. Use the single-channel helpers when you only need one:
import "github.com/CodeSyncr/nimbus/notification"
// Send on all channels (mail + broadcast)
err := notification.Send(&OrderShipped{
OrderID: 42,
UserEmail: "jane@example.com",
UserName: "Jane",
})
// Send mail only
err = notification.SendMail(&OrderShipped{...})
// Broadcast only (via Transmit SSE)
notification.Broadcast(&OrderShipped{...})
Channel Behavior
| Function | Channel | Skip condition |
|---|---|---|
Send(n) | Mail + Broadcast | Skips each channel if nil/empty |
SendMail(n) | Mail only | Skips if ToMail() returns nil or mail.Default is nil |
Broadcast(n) | Broadcast (Transmit SSE) | No-op if channel is empty or Transmit not registered |
Database Notifications
For persistent in-app notifications (bell icon, inbox), use the database.NotificationStore:
import "github.com/CodeSyncr/nimbus/database"
// Create a store (wraps GORM, auto-migrates notifications table)
store := database.NewNotificationStore(db)
// Send a notification
store.Send("user:42", "order.shipped", map[string]any{
"order_id": 42,
"message": "Your order has shipped!",
})
// Retrieve notifications
all, _ := store.All("user:42") // all notifications
unread, _ := store.Unread("user:42") // unread only
read, _ := store.Read("user:42") // read only
// Mark as read
store.MarkAsRead("user:42", notificationID)
store.MarkAllAsRead("user:42")
// Delete
store.Delete("user:42", notificationID)
// Count
count, _ := store.UnreadCount("user:42")
DatabaseNotification Model
type DatabaseNotification struct {
ID uint `gorm:"primaryKey"`
Notifiable string `gorm:"index"` // e.g. "user:42"
Type string // e.g. "order.shipped"
Data JSON // JSON payload
ReadAt *time.Time // nil = unread
CreatedAt time.Time
UpdatedAt time.Time
}
Notifiable Interface
Models can implement Notifiable to provide their notification key:
type Notifiable interface {
NotifiableKey() string // e.g. "user:42"
}
// Example
func (u *User) NotifiableKey() string {
return fmt.Sprintf("user:%d", u.ID)
}
Complete Example
Combining channels with database persistence:
func ShipOrder(order *Order, user *User) error {
n := &OrderShipped{
OrderID: order.ID,
UserEmail: user.Email,
UserName: user.Name,
}
// 1. Send email + broadcast
if err := notification.Send(n); err != nil {
logger.Error("notification failed", "err", err)
}
// 2. Persist to database for in-app inbox
store.Send(user.NotifiableKey(), "order.shipped", map[string]any{
"order_id": order.ID,
"message": fmt.Sprintf("Order #%d has shipped!", order.ID),
})
return nil
}
Slack Channel
Send notifications to Slack via incoming webhooks. Implement SlackNotification:
type DeployCompleted struct {
Environment string
Version string
Duration time.Duration
}
func (n *DeployCompleted) ToMail() *mail.Message { return nil }
func (n *DeployCompleted) ToBroadcast() (string, any) { return "", nil }
func (n *DeployCompleted) ToSlack() *notification.SlackMessage {
return ¬ification.SlackMessage{
Channel: "#deployments",
Text: fmt.Sprintf("Deployed %s to %s", n.Version, n.Environment),
Username: "Deploy Bot",
IconEmoji: ":rocket:",
Attachments: []notification.SlackAttachment{{
Color: "good",
Fields: []notification.SlackField{
{Title: "Version", Value: n.Version, Short: true},
{Title: "Environment", Value: n.Environment, Short: true},
{Title: "Duration", Value: n.Duration.String(), Short: true},
},
}},
}
}
// Setup
slackChannel := notification.NewSlackChannel(os.Getenv("SLACK_WEBHOOK_URL"))
err := slackChannel.Send(&DeployCompleted{...})
Discord Channel
Send notifications to Discord via webhooks. Implement DiscordNotification:
type AlertTriggered struct {
Service string
Message string
Severity string
}
func (n *AlertTriggered) ToMail() *mail.Message { return nil }
func (n *AlertTriggered) ToBroadcast() (string, any) { return "", nil }
func (n *AlertTriggered) ToDiscord() *notification.DiscordMessage {
color := 0x00FF00 // green
if n.Severity == "critical" {
color = 0xFF0000 // red
}
return ¬ification.DiscordMessage{
Username: "Alert Bot",
Embeds: []notification.DiscordEmbed{{
Title: fmt.Sprintf("Alert: %s", n.Service),
Description: n.Message,
Color: color,
Fields: []notification.DiscordEmbedField{
{Name: "Service", Value: n.Service, Inline: true},
{Name: "Severity", Value: n.Severity, Inline: true},
},
Timestamp: time.Now().Format(time.RFC3339),
}},
}
}
// Setup
discordChannel := notification.NewDiscordChannel(os.Getenv("DISCORD_WEBHOOK_URL"))
err := discordChannel.Send(&AlertTriggered{...})
Channel Summary
| Channel | Transport | Setup | Use Case |
|---|---|---|---|
| SMTP/provider | mail package config | User-facing email alerts | |
| Broadcast | SSE (Transmit) | Transmit plugin | Real-time in-app push |
| Database | SQL table | NewNotificationStore(db) | In-app notification center |
| Slack | Webhook HTTP | NewSlackChannel(url) | Team alerts, deploys |
| Discord | Webhook HTTP | NewDiscordChannel(url) | Community alerts, monitoring |
API Endpoint Example
// GET /api/notifications
func ListNotifications(c *http.Context) error {
user := auth.UserFromContext(c.Request.Context())
store := database.NewNotificationStore(database.Get())
notifications, _ := store.Unread(fmt.Sprintf("user:%s", user.GetID()))
return c.JSON(200, notifications)
}
// POST /api/notifications/:id/read
func MarkRead(c *http.Context) error {
user := auth.UserFromContext(c.Request.Context())
id, _ := strconv.Atoi(c.Param("id"))
store := database.NewNotificationStore(database.Get())
store.MarkAsRead(fmt.Sprintf("user:%s", user.GetID()), uint(id))
return c.JSON(200, map[string]string{"status": "ok"})
}