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

FunctionChannelSkip condition
Send(n)Mail + BroadcastSkips each channel if nil/empty
SendMail(n)Mail onlySkips 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

ChannelTransportSetupUse Case
MailSMTP/providermail package configUser-facing email alerts
BroadcastSSE (Transmit)Transmit pluginReal-time in-app push
DatabaseSQL tableNewNotificationStore(db)In-app notification center
SlackWebhook HTTPNewSlackChannel(url)Team alerts, deploys
DiscordWebhook HTTPNewDiscordChannel(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"})
}