add notification
go tool gpsystem add notification <module> <name>
Creates a notification in internal/modules/<module>/notifications/<name>.go: a payload struct, Via (defaulting to [database, mail]), a ToMail and a ToDatabase, plus its templates/<name>.html.tmpl + templates/<name>.txt.tmpl. On a project's first notification (in any module) it also wires the shared notify.Sender into every entry point and adds the notifications table migration. Idempotent: a second notification, in the same or a different module, reuses the wiring.
go tool gpsystem add notification contact orderShipped
Why the split from add mail
add mail is for content sent straight to an address, with no recipient concept: a contact-form confirmation, a password-reset link. add notification is for content addressed to a recipient (a user, identified by a stable id), possibly delivered on several channels at once. The two are independent and compose: a notification's ToMail builds on the same mail.Message/mail.Template a mail notifier's Notify does; see the Notifications page and its worked example for a case using both side by side.
What it generates
package notifications
//go:embed templates/ordershipped.html.tmpl templates/ordershipped.txt.tmpl
var orderShippedTemplatesFS embed.FS
type OrderShipped struct {
}
var (
_ notify.Notification = OrderShipped{}
_ notify.Mailable = OrderShipped{}
_ notify.Databasable = OrderShipped{}
)
func (OrderShipped) NotificationName() string { return "contact.order_shipped" }
func (OrderShipped) Via(ctx context.Context, r notify.Recipient) []string {
return []string{notify.ChannelDatabase, notify.ChannelMail}
}
func (n OrderShipped) ToMail(ctx context.Context, r notify.Recipient) (*kitmail.Message, error) {
return kitmail.NewMessage().
WithSubject("TODO: subject for OrderShipped").
WithBody(kitmail.Template(orderShippedTemplatesFS, "templates/ordershipped.html.tmpl", n).
WithTextTemplate("templates/ordershipped.txt.tmpl")), nil
// no WithTo: the mail channel routes to r automatically
}
func (n OrderShipped) ToDatabase(ctx context.Context, r notify.Recipient) (any, error) {
return n, nil
}
Plus a test using a memory mail channel and a memory database store, and two placeholder templates you fill in. The payload struct starts empty, the same way add event and add mail generate empty structs: add whatever fields your templates need, and add notify.ChannelBroadcast to Via once add realtime has run.
On the first notification in the project:
type Dependencies struct {
// ...
Notifier notify.Sender
// gpsystem:dependencies
}
mailer := smtp.MustNewIfConfigured(ctx, cfg.Mail)
notifier := notify.NewHub(
notify.NewMailChannel(mailer),
notifydatabase.NewChannel(notifydatabase.NewStore(pg.NewDB(pool))),
)
// ...
deps := contact.Dependencies{
// ...
Notifier: notifier,
// gpsystem:dependencies-init
}
notifier is constructed once per entry point (the same pattern as the shared mailer and database pool), and shared across every module with a notification. The database channel always persists through the kit's pgx executor (pg.NewDB(pool)), regardless of the project's --db choice for its business modules: pool is in scope everywhere the notifier is built, whereas a bun.DB is only constructed later, inside the per-module Dependencies literal.
If add realtime has already run, the notifier construction also builds a notify/broadcast.Publisher and adds notify.NewBroadcastChannel(...) to the Hub, plus its closer:
pub := broadcast.MustNewPublisher(ctx, cfg.Realtime.Redis, cfg.Realtime.Broadcast)
notifier := notify.NewHub(
notify.NewMailChannel(mailer),
notifydatabase.NewChannel(notifydatabase.NewStore(pg.NewDB(pool))),
notify.NewBroadcastChannel(pub, cfg.Realtime.Broadcast.ChannelPrefix),
)
add realtime runs after that first notification already exists, later notifications in other modules still reuse the earlier (mail+database-only) construction rather than emitting a second, conflicting one: add the broadcast channel to that entry point by hand, or run add realtime before your first add notification to get it wired automatically everywhere.Also wires the migration (idempotent: skipped if a notifications table migration already exists):
CREATE TABLE notifications (
id uuid PRIMARY KEY, recipient_id text NOT NULL, name text NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb, read_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);
Sending it
Nothing calls the notification automatically: send it from a service (synchronous) or a listener (asynchronous, via add event / add listener):
return deps.Notifier.Send(ctx,
notify.Recipient{ID: identity.Subject, Email: identity.Email, Name: identity.Username},
contactnotifications.OrderShipped{ /* ... */ },
)
Older projects
Like add mail and add db, add notification writes at the gpsystem:pools, gpsystem:dependencies-init/gpsystem:deps-init(<module>), gpsystem:dependencies and gpsystem:config anchors; a project generated before those anchors existed needs them added by hand first (see a freshly generated project for placement).