Notifykit is a small Go toolkit for templated notifications. It handles receiver dispatch, retries, template rendering, and reusable webhook/email targets.
Your application keeps its own config, event types, receiver definitions, and render data. Notifykit only needs a notify.Notification implementation.
For simple synchronous delivery, use notify.SendTo. It accepts receivers directly, so small programs do not need to build a receiver map.
package main
import (
"context"
"log/slog"
"os"
"time"
"github.com/containeroo/notifykit/notify"
"github.com/containeroo/notifykit/templates"
"github.com/containeroo/notifykit/targets/webhook"
)
type Alert struct {
IDValue string
Service string
Status string
}
func (a Alert) ID() string { return a.IDValue }
func (a Alert) Data(receiver string, vars map[string]any, subject string) any {
return map[string]any{
"ID": a.IDValue,
"Service": a.Service,
"Status": a.Status,
"Subject": subject,
"Receiver": receiver,
"Vars": vars,
}
}
func main() {
ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
subject, err := templates.ParseStringTemplate("subject", `{{ .Service }} is {{ .Status }}`)
if err != nil {
panic(err)
}
body, err := templates.ParseTemplate("webhook", `{"text": {{ .Subject | json }}}`)
if err != nil {
panic(err)
}
target := webhook.New(
webhook.WithURL("https://example.com/webhook"),
webhook.WithSubjectTemplate(subject),
webhook.WithTemplate(body),
webhook.WithClient(webhook.NewClient(10*time.Second)),
webhook.WithLogger(logger),
webhook.WithValidateJSON(),
)
receiver := notify.NewReceiver("ops", target).
WithRetry(notify.RetryConfig{
Count: 2,
Delay: time.Second,
})
err = notify.SendTo(ctx, Alert{
IDValue: "alert-1",
Service: "api",
Status: "down",
}, receiver)
if err != nil {
panic(err)
}
}Runnable examples are available in:
examples/single/ synchronous send to one receiver
examples/multiple/ synchronous send to multiple receivers
Run them with:
go run ./examples/single
go run ./examples/multiplenotify.NewReceiver and the fluent receiver methods are convenience helpers for simple setup:
receiver := notify.NewReceiver("ops", slackTarget, emailTarget).
WithName("Operations").
WithVars(map[string]any{"channel": "alerts"}).
WithRetry(notify.RetryConfig{Count: 2, Delay: time.Second})
err := notify.SendTo(ctx, alert, receiver)Available helpers:
func NewReceiver(id ReceiverID, targets ...Target) *Receiver
func NewReceivers(receivers ...*Receiver) Receivers
func SendTo(ctx context.Context, notification Notification, receivers ...*Receiver) error
func (r *Receiver) WithName(name string) *Receiver
func (r *Receiver) WithVars(vars map[string]any) *Receiver
func (r *Receiver) WithRetry(cfg RetryConfig) *Receiver
func (r *Receiver) WithTargets(targets ...Target) *ReceiverSendTo is a small wrapper around Send: it builds a Receivers map from the provided receivers, uses a discard logger for Notifykit internals, resolves routing, and returns after delivery completes.
Webhook and email targets use functional options for simple construction.
client := webhook.NewClient(
10*time.Second,
webhook.WithProxyFromEnvironment(),
webhook.WithSkipTLSVerify(),
)
webhookTarget := webhook.New(
webhook.WithName("slack-alerts"),
webhook.WithURL("https://example.com/webhook"),
webhook.WithClient(client),
webhook.WithSubjectTemplate(subject),
webhook.WithTemplate(body),
webhook.WithValidateJSON(),
)
emailTarget := email.New(
email.WithHost("smtp.example.com"),
email.WithPort(587),
email.WithCredentials("user", "pass"),
email.WithFrom("alerts@example.com"),
email.WithTo("ops@example.com"),
email.WithCC("lead@example.com"),
email.WithBCC("audit@example.com"),
email.WithSubjectTemplate(subject),
email.WithTemplate(body),
)For config-driven applications, use a notify.Receivers map directly. The map key is the receiver ID used for routing.
receivers := notify.Receivers{
"ops": {
Name: "Operations",
Retry: notify.RetryConfig{
Count: 2, // two retries, three total attempts
Delay: time.Second,
},
Vars: map[string]any{
"channel": "alerts",
},
Targets: []notify.Target{
slackWebhook,
emailTarget,
},
},
}
err := notify.Send(ctx, alert, receivers, logger)For queued asynchronous delivery, use notify.NewManager, start it once, and enqueue notifications over time.
manager, err := notify.NewManager(receivers, logger)
if err != nil {
panic(err)
}
if err := manager.Start(ctx); err != nil {
panic(err)
}
queueID, err := manager.Enqueue(ctx, alert)
if err != nil {
panic(err)
}
fmt.Println("queued notification", queueID)flowchart LR
A[Application event] --> B[notify.Notification]
B --> C{Usage style}
C --> D[notify.SendTo]
C --> E[notify.Send]
C --> F[Manager.Enqueue]
F --> G[Store]
F --> H[Mailbox]
H --> I[Dispatcher]
D --> J[Build receiver map]
E --> K[Resolve receivers]
I --> K
J --> K
K --> L[Delivery]
L --> M[Retry]
M --> N[Target]
N --> O[Render subject]
O --> P[Render body with subject]
P --> Q[Send webhook or email]
notify/ queue, dispatcher, receivers, retries, and target interfaces
templates/ template loading, parsing, and rendering
targets/webhook/ HTTP webhook target
targets/email/ SMTP email target
ids/ UUIDv7 generator
Applications implement this interface:
type Notification interface {
ID() string
Data(receiver string, vars map[string]any, subject string) any
}ID returns a stable notification identifier for logs and delivery tracing.
Data returns the template context. Webhook and email targets call it twice: first with an empty subject to render the subject template, then with the rendered subject so the body can use .Subject.
To select specific receivers, also implement notify.ReceiverRouter:
type ReceiverRouter interface {
ReceiverIDs() []ReceiverID
}ReceiverIDs controls routing. The returned values are matched against the keys in the receiver map passed to notify.Send, notify.SendTo, or notify.NewManager.
func (a Alert) ReceiverIDs() []notify.ReceiverID {
return []notify.ReceiverID{"ops"}
}Routing behavior:
nil or empty ReceiverIDs() send to all configured receivers
[]ReceiverID{"ops"} send to receiver ID "ops"
[]ReceiverID{"ops", "dev"} send to both receiver IDs
unknown receiver ID skip that receiver and log a warning
For compatibility, notifications that still implement ReceiverNames() []string are also supported, but new code should use ReceiverIDs.
A receiver groups one or more delivery targets and optional receiver-scoped settings.
receiver := notify.NewReceiver("ops", slackWebhook, emailTarget).
WithName("Operations").
WithVars(map[string]any{"channel": "alerts"}).
WithRetry(notify.RetryConfig{Count: 2, Delay: time.Second})Receiver.ID is the routing identifier. Receiver.Name is passed into the notification payload as the receiver name. When Name is empty, Notifykit defaults it to the receiver ID.
Webhook targets own HTTP-specific dependencies. The manager keeps its own logger for queueing and dispatch logs, while each webhook target may receive a target-specific Logger and Client.
This keeps notify.Manager transport-agnostic and still lets applications provide a custom *http.Client for timeouts, transports, proxies, tracing, mTLS, or tests.
client := webhook.NewClient(
5*time.Second,
webhook.WithProxyFromEnvironment(),
)
target := webhook.New(
webhook.WithURL("https://example.com/webhook"),
webhook.WithSubjectTemplate(subject),
webhook.WithTemplate(body),
webhook.WithClient(client),
webhook.WithLogger(logger),
webhook.WithValidateJSON(),
)By default, webhook.NewClient does not use proxy environment variables. Add webhook.WithProxyFromEnvironment() when proxy support should be enabled. Add webhook.WithSkipTLSVerify() only for local development or trusted private endpoints with self-signed certificates.
Email targets support primary, CC, and BCC recipients.
target := email.New(
email.WithHost("smtp.example.com"),
email.WithFrom("alerts@example.com"),
email.WithTo("ops@example.com"),
email.WithCC("lead@example.com"),
email.WithBCC("audit@example.com"),
email.WithSubjectTemplate(subject),
email.WithTemplate(body),
)To and CC are written as message headers. BCC recipients are sent as SMTP envelope recipients but are not written to the message headers.
Templates use Go text/template plus Notifykit's built-in helper functions. The default helper map includes json, default, withPrefix, optional, and when, which are enough for the bundled Slack and email examples.
subject, err := templates.ParseStringTemplate("subject", `{{ .Service }} is {{ .Status }}`)
body, err := templates.LoadSource(templateFS, "builtin:slack")Missing map keys fail by default. Use WithMissingKey for looser templates:
tmpl, err := templates.ParseStringTemplate(
"subject",
`{{ .Service }}`,
templates.WithMissingKey(templates.MissingKeyDefault),
)Run the full local check suite with:
make testThis runs go fmt, go vet, and go test -covermode=atomic ./....
Keep these parts in your application:
- config parsing
- event types
- render data structs
- built-in template aliases and defaults
- database persistence
- metrics and audit logging
Notifykit owns only the notification mechanics.