go-notificationgo-notification
Features

On-Demand Notifications

Send to a raw address without a persisted user — for anonymous, admin, or one-off notifications.

Sometimes you need to send to a raw address without having a real user record:

  • An admin alert to an email or chat target not tied to a user.
  • A signup confirmation to an email that doesn't yet have a user record.
  • An ad-hoc "send this to myself" from a CLI tool.

There is no special SendOnDemand method. Because Notifiable is just a two-method interface, the idiom is to pass a small ad-hoc notifiable to the normal Send.

An ad-hoc notifiable

Define a tiny type (once) whose routes come from a map:

main.go
type OnDemand struct {
    id     string
    routes map[string]string
}

func (o OnDemand) GetID() string { return o.id }
func (o OnDemand) RouteNotificationFor(channel string) string {
    return o.routes[channel] // "" when absent → channel skipped
}

// helper
func To(routes map[string]string) OnDemand {
    return OnDemand{id: "on-demand", routes: routes}
}

Then send to it like any other recipient:

main.go
notifier.Send(ctx, To(map[string]string{
    "mail":     "admin@example.com",
    "chat":     "#ops-alerts",
    "whatsapp": "+628123456789",
}), IncidentStarted{Severity: "sev-1", Service: "checkout"})

How channels are chosen

The notification's Via() still decides which channels run. The ad-hoc notifiable only supplies routes for those channels — a channel whose route is "" is skipped. To narrow the channels for a single send regardless of Via(), use the Via send option:

main.go
notifier.Send(ctx, recipient, IncidentStarted{...}, notification.Via("mail", "chat"))

Overriding one route for a real user

To send to a different address than a user's default for one call, wrap them in an ad-hoc notifiable (or model the alternate address explicitly on your user type, e.g. NotificationEmail vs LoginEmail — usually clearer):

main.go
notifier.Send(ctx, To(map[string]string{"mail": "custom@example.com"}), OrderShipped{...})

Multiple push tokens / topics

Routing returns a single string, so for FCM multicast or topics, set them in the notification's ToPush (via SetTokens/SetTopic) rather than through the route — see Push.

When it's a bad fit

  • High-volume fan-out to dynamic addresses — works, but a proper Notifiable modeled from your data is cleaner.
  • Anything you'll call from three or more places — define a domain type once instead of scattering To(map…) literals.

Tests

An ad-hoc notifiable plus a recording test channel is the cleanest way to assert what Via() and the To* methods produce, with no fake user wiring:

main.go
func TestOrderShippedRendering(t *testing.T) {
    var captured *mail.Message
    notifier := notification.New(notification.Config{Async: notification.BoolPtr(false)})
    notifier.RegisterChannel(channel.Func("mail", func(_ context.Context, _ notification.Notifiable, msg any) error {
        captured = msg.(*mail.Message)
        return nil
    }))

    _ = notifier.Send(ctx, To(map[string]string{"mail": "test@example.com"}), OrderShipped{OrderID: "A-1"})

    require.Equal(t, "Your order shipped", captured.Subject)
}