go-notificationgo-notification
API Reference

Notification Interface

The contract every notification type implements.

A notification is a struct that carries the data for a single message and implements the Notification interface.

Interface

main.go
type Notification interface {
    Via(notifiable Notifiable) []string
}

Just one required method. Everything else — ToMail, ToWhatsApp, ToSMS, etc. — is optional and discovered by each channel via Go type assertion at send time.

Via

Returns the channel names this notification should go through for the given recipient. It receives the Notifiable so routing can depend on the recipient:

main.go
func (n OrderShipped) Via(notifiable notification.Notifiable) []string {
    user := notifiable.(User)

    channels := []string{"database"} // in-app is always on
    if user.EmailVerified { channels = append(channels, "mail") }
    if user.WantsWhatsApp { channels = append(channels, "whatsapp") }
    return channels
}

Per-channel render methods

Each channel expects one method that produces its message shape. The full signatures are in Message Types. The surface:

main.go
func (n MyNotification) ToMail(u Notifiable)     *mail.Message
func (n MyNotification) ToWhatsApp(u Notifiable) *whatsapp.Message
func (n MyNotification) ToSMS(u Notifiable)      *sms.Message
func (n MyNotification) ToPush(u Notifiable)     *push.Message
func (n MyNotification) ToChat(u Notifiable)     *chat.Message
func (n MyNotification) ToDatabase(u Notifiable) *database.Message
func (n MyNotification) ToWebhook(u Notifiable)  *webhook.Message

There is no ToSlack/ToTelegram/ToDiscord/ToTeams. Slack, Telegram, Discord, and Microsoft Teams all share a single chat channel: implement ToChat returning a *chat.Message, and set provider-specific extras (Slack attachments, Discord embeds, Telegram parse mode) on that one message. Also note the SMS method is ToSMS (uppercase), not ToSms — a misspelling is silently not discovered.

You only implement the ones listed in your Via(). If Via returns []string{"mail", "database"}, you need ToMail and ToDatabase — the rest don't need to exist. If a channel in Via has no matching To<Channel> method, that channel is skipped (ErrNoFormatter, logged at debug level).

Custom channels: ChannelFormatter

For channels beyond the built-in ones, implement the optional ChannelFormatter interface on the notification. The notifier calls it only when the channel doesn't implement Formatter:

main.go
type ChannelFormatter interface {
    Format(channel string, notifiable Notifiable) any
}

Not the same as Formatter

Don't confuse this with Formatter on the channel side. Formatter (Format(n, notifiable) (any, error)) lives on the channel and is how every built-in driver works. ChannelFormatter lives on the notification and is a fallback used only when the channel doesn't implement its own Formatter.

See Writing a Custom Channel.

Channel name matching

The Via() strings must match the names your registered channels report from Name(). An unregistered name surfaces through OnError:

main.go
// Registered under its own Name() — "mail" for a default mail driver.
notifier.RegisterChannel(mailgun.New(mailgun.Config{ /* ... */ }))

// "email" isn't registered → dispatch fails with ErrNoChannel.
func (n OrderShipped) Via(u notification.Notifiable) []string {
    return []string{"email"}
}

Notifications carry their data

Notifications are typically struct values with all data passed in at construction:

main.go
type OrderShipped struct {
    OrderID          string
    TrackingURL      string
    EstimatedArrival time.Time
}

notifier.Send(ctx, user, OrderShipped{
    OrderID:          "A-1024",
    TrackingURL:      "https://...",
    EstimatedArrival: time.Now().Add(48 * time.Hour),
})

Testing a notification

Via and the To<Channel> methods are plain functions — test them without a notifier. Message fields like Subject are struct fields (not methods):

main.go
func TestOrderShippedVia(t *testing.T) {
    user := User{EmailVerified: true, WantsWhatsApp: false}
    got := OrderShipped{}.Via(user)
    require.Equal(t, []string{"database", "mail"}, got)
}

func TestOrderShippedToMail(t *testing.T) {
    msg := OrderShipped{OrderID: "A-1"}.ToMail(User{})
    require.Contains(t, msg.Subject, "A-1")
}