Notifications

> Send notifications across mail, database, broadcast, and Slack from a single definition.

The notification package lets you define a single notification and deliver it over any combination of channels — mail, database, broadcast (WebSocket), Slack. You write ToMail, ToDatabase, ToBroadcast, ToSlack as needed; the manager dispatches to the right channel driver.

Import path: github.com/velocitykode/velocity/notification

Core interfaces

type Notification interface {
    Via(notifiable any) []string  // channel names, e.g. "mail", "database"
}

type Notifiable interface {
    NotificationRoute(channel string) string  // address for this channel
}

type Channel interface {
    Send(ctx context.Context, notifiable any, notification Notification) error
}

A notification declares which channels it targets. A notifiable (usually your User model) tells each channel where to deliver. A channel performs the actual delivery.

Defining a notification

type OrderShipped struct {
    OrderID  int
    Carrier  string
    TrackURL string
}

func (n *OrderShipped) Via(_ any) []string {
    return []string{"mail", "database"}
}

func (n *OrderShipped) ToMail(_ any) *notification.MailMessage {
    return notification.NewMailMessage().
        Subject("Your order has shipped").
        Greeting("Hi there!").
        Line("Your order is on its way.").
        Action("Track your package", n.TrackURL).
        Line(fmt.Sprintf("Carrier: %s", n.Carrier)).
        Outro("Thanks for shopping with us.")
}

func (n *OrderShipped) ToDatabase(_ any) *notification.DatabaseMessage {
    return notification.NewDatabaseMessage("orders.shipped").
        Set("order_id", n.OrderID).
        Set("carrier", n.Carrier).
        Set("track_url", n.TrackURL)
}

Defining a notifiable

type User struct {
    ID    int
    Email string
}

func (u *User) NotificationRoute(channel string) string {
    switch channel {
    case "mail":
        return u.Email
    case "database":
        return strconv.Itoa(u.ID)
    case "slack":
        return u.SlackWebhookURL
    }
    return ""
}

Sending

The manager fans out to each channel in Via():

mgr := notification.NewManager()

err := mgr.Send(ctx, user, &OrderShipped{
    OrderID:  42,
    Carrier:  "UPS",
    TrackURL: "https://ups.com/track/42",
})

Send to many recipients at once:

mgr.SendMany(ctx, []any{user1, user2, user3}, notif)

SendMany accumulates errors — one failed recipient doesn’t prevent the others from receiving the notification.

Channels

The package ships four built-in channels. Register the ones you need:

import "github.com/velocitykode/velocity/notification/allchannels"

allchannels.Register()  // registers mail, database, broadcast, slack

Or cherry-pick:

notification.RegisterChannel("mail", func() (notification.Channel, error) {
    return channels.NewMailChannel(), nil
})

After registration, mgr.Channel("mail") returns the driver; the manager uses this internally when a notification lists "mail" in Via().

Mail channel

Notifications implementing MailNotification (have a ToMail method) build a *MailMessage and send it through the mail package.

Message builder covers the common shape — greeting, body lines, call-to-action button, outro — plus full HTML/text body override when you need custom templates:

return notification.NewMailMessage().
    From("no-reply@example.com", "Example").
    To(user.Email).
    Subject("Welcome").
    Greeting("Hi "+user.Name).
    Line("Thanks for signing up.").
    Action("Get started", "https://example.com/onboard").
    Outro("Need help? Reply to this email.")

For complete control:

return notification.NewMailMessage().
    To(user.Email).
    Subject("Receipt").
    HTMLBody(renderedHTML).
    TextBody(plainText).
    AttachData(pdfBytes, "receipt.pdf", "application/pdf")

Database channel

Stores the notification as a row in a notifications table (or whichever table your store implements). Use this for in-app notification inboxes.

return notification.NewDatabaseMessage("billing.invoice.paid").
    Set("invoice_id", inv.ID).
    Set("amount", inv.Total)

The Type field is a logical name; Data is serialized as JSON.

Broadcast channel

Delivers to real-time WebSocket subscribers via the broadcast package. The notifiable’s route should be a channel name ("private-user.42", "orders").

return notification.NewBroadcastMessage("order.shipped").
    On("private-user."+strconv.Itoa(n.UserID)).
    Set("order_id", n.OrderID).
    Set("track_url", n.TrackURL)

Slack channel

Posts to a Slack webhook URL returned by the notifiable:

return notification.NewSlackMessage().
    Content("New signup: "+user.Email).
    AsUser("Signups Bot").
    WithIcon(":wave:").
    Attachment(func(a *notification.SlackAttachment) {
        a.Color = "good"
        a.Title = user.Name
        a.Field("Plan", user.Plan, true)
        a.Field("Trial ends", user.TrialEnd.Format("2006-01-02"), true)
    })

Events

Every send emits one of two events:

  • *notification.NotificationSent — on successful delivery
  • *notification.NotificationFailed — on delivery error

Wire them up via the manager:

mgr.SetEventDispatcher(func(event any) error {
    return v.Events.Dispatch(event)
})

Useful for delivery metrics, retry workers, or audit logs.

Custom channels

Implement the Channel interface and register it:

type PushChannel struct { /* APNs/FCM client */ }

func (c *PushChannel) Send(ctx context.Context, notifiable any, n notification.Notification) error {
    // 1. Assert n implements your channel-specific interface (e.g. PushNotification)
    // 2. Use notifiable.(notification.Notifiable).NotificationRoute("push") for the token
    // 3. Send it
    return nil
}

notification.RegisterChannel("push", func() (notification.Channel, error) {
    return &PushChannel{/* ... */}, nil
})

From then on, any notification listing "push" in Via() goes through your channel.