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:
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:
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:
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):
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
Notifiablemodeled 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:
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)
}