go-notificationgo-notification
Examples

Full E-Commerce Example

A realistic multi-channel notification setup for an e-commerce app — order lifecycle, OTPs, admin alerts.

This brings together everything. A small e-commerce backend that uses go-notification for:

  • Customer comms — order shipped, delivery confirmed.
  • OTP — 2FA via SMS.
  • Admin alerts — new signup, payment failure.
  • In-app — every event is also stored in the database channel.

Domain types

main.go
type Customer struct {
    ID            int64
    Name, Email   string
    Phone         string // E.164
    DeviceToken   string
    WantsMail     bool
    WantsWhatsApp bool
}

func (c Customer) GetID() string { return strconv.FormatInt(c.ID, 10) }

func (c Customer) RouteNotificationFor(channel string) string {
    switch channel {
    case "mail":
        return c.Email
    case "whatsapp":
        return c.Phone
    case "sms":
        return c.Phone
    case "push":
        return c.DeviceToken
    case "database":
        return c.GetID()
    }
    return ""
}

type Admin struct {
    ID    int64
    Email string
    Slack string // channel ID like "#ops"
}

func (a Admin) GetID() string { return strconv.FormatInt(a.ID, 10) }

func (a Admin) RouteNotificationFor(channel string) string {
    switch channel {
    case "mail":
        return a.Email
    case "chat":
        return a.Slack
    case "database":
        return a.GetID()
    }
    return ""
}

Notifications

main.go
type OrderShipped struct {
    OrderID     string
    TrackingURL string
}

func (n OrderShipped) Via(notifiable notification.Notifiable) []string {
    c := notifiable.(Customer)
    channels := []string{"database", "push"}
    if c.WantsMail     { channels = append(channels, "mail") }
    if c.WantsWhatsApp { channels = append(channels, "whatsapp") }
    return channels
}

func (n OrderShipped) ToMail(u notification.Notifiable) *mail.Message {
    return mail.NewMessage().
        SetSubject("Your order shipped").
        Line("Order " + n.OrderID + " is on its way.").
        Action("Track package", n.TrackingURL)
}

func (n OrderShipped) ToWhatsApp(u notification.Notifiable) *whatsapp.Message {
    return whatsapp.NewMessage().
        SetText("📦 Order *" + n.OrderID + "* shipped. Track: " + n.TrackingURL)
}

func (n OrderShipped) ToPush(u notification.Notifiable) *push.Message {
    return push.NewMessage().
        SetTitle("Order shipped").
        SetBody("Order " + n.OrderID + " is on its way.").
        SetData("order_id", n.OrderID)
}

func (n OrderShipped) ToDatabase(u notification.Notifiable) *database.Message {
    return database.NewMessage().
        SetType("order.shipped").
        SetTitle("Order shipped").
        SetBody("Order " + n.OrderID + " is on its way.").
        AddData("order_id", n.OrderID).
        AddData("tracking_url", n.TrackingURL)
}


type LoginOTP struct {
    Code string
}

func (n LoginOTP) Via(_ notification.Notifiable) []string {
    return []string{"sms"} // OTPs only via SMS for reliability
}

func (n LoginOTP) ToSMS(_ notification.Notifiable) *sms.Message {
    return sms.NewMessage().SetText("Your code: " + n.Code + ". Valid 5 minutes.")
}


type PaymentFailed struct {
    OrderID string
    Reason  string
}

func (n PaymentFailed) Via(_ notification.Notifiable) []string {
    return []string{"chat", "database", "mail"}
}

func (n PaymentFailed) ToChat(_ notification.Notifiable) *chat.Message {
    return chat.NewMessage().
        SetText(":warning: Payment failure").
        AddSlackAttachment(chat.SlackAttachment{
            Color: "danger",
            Fields: []chat.SlackField{
                {Title: "Order", Value: n.OrderID, Short: true},
                {Title: "Reason", Value: n.Reason, Short: true},
            },
        })
}

func (n PaymentFailed) ToMail(_ notification.Notifiable) *mail.Message {
    return mail.NewMessage().
        SetSubject("[ops] Payment failed: " + n.OrderID).
        Line("A payment failed and needs manual review.").
        Line("Reason: " + n.Reason)
}

func (n PaymentFailed) ToDatabase(_ notification.Notifiable) *database.Message {
    return database.NewMessage().
        SetType("payment.failed").
        SetTitle("Payment failed").
        SetBody("Order " + n.OrderID + " failed: " + n.Reason).
        AddData("order_id", n.OrderID)
}

Boot

main.go
func NewNotifier(db *sql.DB, env string) *notification.Notifier {
    n := notification.New(notification.Config{
        WorkerPool: 8,
        MaxRetries: 3,
        RetryDelay: time.Second,
        OnError: func(_ context.Context, no notification.Notifiable, channel string, err error) {
            slog.Error("notify failed", "channel", channel, "err", err)
        },
        OnSuccess: func(_ context.Context, no notification.Notifiable, channel string) {
            metrics.NotificationSent.WithLabelValues(channel).Inc()
        },
    })

    // Email — production via SendGrid, dev via Mailtrap
    if env == "production" {
        n.RegisterChannel(sendgrid.New(sendgrid.Config{
            APIKey: os.Getenv("SENDGRID_API_KEY"),
            From:   mail.Address{Name: "Shop", Address: "noreply@shop.example.com"},
        }))
    } else {
        inboxID, _ := strconv.Atoi(os.Getenv("MAILTRAP_INBOX_ID"))
        n.RegisterChannel(mailtrap.New(mailtrap.Config{
            Sandbox:  true,
            APIToken: os.Getenv("MAILTRAP_API_TOKEN"),
            InboxID:  inboxID,
            From:     mail.Address{Name: "Shop Dev", Address: "dev@shop.example.com"},
        }))
    }

    // WhatsApp — self-hosted via WAHA
    n.RegisterChannel(waha.New(waha.Config{
        BaseURL:   os.Getenv("WAHA_BASE_URL"),
        APIKey:    os.Getenv("WAHA_API_KEY"),
        SessionID: "default",
    }))

    // SMS — Twilio
    n.RegisterChannel(twilio.New(twilio.Config{
        AccountSID: os.Getenv("TWILIO_ACCOUNT_SID"),
        AuthToken:  os.Getenv("TWILIO_AUTH_TOKEN"),
        From:       "+14155238886",
    }))

    // Push — FCM (constructor returns an error, so capture it)
    pushDriver, err := fcm.New(fcm.Config{
        ProjectID:          os.Getenv("FCM_PROJECT_ID"),
        ServiceAccountJSON: []byte(os.Getenv("FCM_SERVICE_ACCOUNT_JSON")),
    })
    if err != nil {
        panic(err)
    }
    n.RegisterChannel(pushDriver)

    // Ops chat — Slack incoming webhook
    n.RegisterChannel(slack.New(slack.Config{
        WebhookURL: os.Getenv("OPS_SLACK_WEBHOOK"),
    }))

    // Database — in-app notifications
    if err := migrate.Up(context.Background(), db, database.DialectPostgreSQL, "notifications"); err != nil {
        panic(err)
    }
    store := database.NewSQLStore(db, database.DialectPostgreSQL)
    n.RegisterChannel(database.New(database.Config{Store: store}))

    return n
}

Trigger points

main.go
// In the order service
func (s *OrderService) markShipped(ctx context.Context, o *Order) error {
    // ... DB updates ...
    s.notifier.Send(ctx, o.Customer, OrderShipped{
        OrderID:     o.ID,
        TrackingURL: s.trackingURL(o),
    })
    return nil
}

// In the auth service
func (s *AuthService) requestOTP(ctx context.Context, c Customer) error {
    code := s.generateOTP(c.ID)
    return s.notifier.Send(ctx, c, LoginOTP{Code: code})
}

// In the payment service
func (s *PaymentService) onFailure(ctx context.Context, o *Order, reason string) {
    // Alert all admins
    s.notifier.SendMulti(ctx, s.admins, PaymentFailed{
        OrderID: o.ID,
        Reason:  reason,
    })
}

Observability

Metrics worth tracking:

  • notifications_sent_total{channel, type} — successful deliveries.
  • notifications_failed_total{channel, type} — post-retry failures.
  • notifications_retries_total{channel, type} — a spike here predicts an upstream outage.
  • notifications_duration_seconds{channel} — per-channel latency.

Successful deliveries and post-retry failures can be wired up from the OnSuccess and OnError callbacks on notification.Config.

What you get

With ~200 lines of notification code, this app now has:

  • Four outbound channels to customers (mail, WhatsApp, push, SMS).
  • Ops alerts in Slack.
  • Every event stored as an in-app notification.
  • Retries + rate limits + secret redaction for free.
  • A dev/prod split where dev never hits real customers.

And if tomorrow you want to add Telegram, it's three lines: register the Telegram driver, add a ToChat() method to the notifications (Slack, Telegram, Discord, and Teams all share *chat.Message), and add "telegram" to Via().