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
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
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
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
// 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().