{"id":51401956,"url":"https://github.com/containeroo/notifykit","last_synced_at":"2026-07-04T07:33:40.293Z","repository":{"id":367940009,"uuid":"1280435837","full_name":"containeroo/notifykit","owner":"containeroo","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-28T10:11:21.000Z","size":66,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-28T12:08:26.320Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/containeroo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-25T15:23:54.000Z","updated_at":"2026-06-28T10:11:20.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/containeroo/notifykit","commit_stats":null,"previous_names":["containeroo/notifykit"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/containeroo/notifykit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fnotifykit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fnotifykit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fnotifykit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fnotifykit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/containeroo","download_url":"https://codeload.github.com/containeroo/notifykit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fnotifykit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35114172,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-04T07:33:39.658Z","updated_at":"2026-07-04T07:33:40.277Z","avatar_url":"https://github.com/containeroo.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Notifykit\n\nNotifykit is a small Go toolkit for templated notifications. It handles receiver dispatch, retries, template rendering, and reusable webhook/email targets.\n\nYour application keeps its own config, event types, receiver definitions, and render data. Notifykit only needs a `notify.Notification` implementation.\n\n## Simple example\n\nFor simple synchronous delivery, use `notify.SendTo`. It accepts receivers directly, so small programs do not need to build a receiver map.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log/slog\"\n    \"os\"\n    \"time\"\n\n    \"github.com/containeroo/notifykit/notify\"\n    \"github.com/containeroo/notifykit/templates\"\n    \"github.com/containeroo/notifykit/targets/webhook\"\n)\n\ntype Alert struct {\n    IDValue string\n    Service string\n    Status  string\n}\n\nfunc (a Alert) ID() string { return a.IDValue }\n\nfunc (a Alert) Data(receiver string, customData map[string]any, title string) any {\n    return map[string]any{\n        \"ID\":       a.IDValue,\n        \"Service\":  a.Service,\n        \"Status\":   a.Status,\n        \"Title\":    title,\n        \"Receiver\": receiver,\n        \"CustomData\": customData,\n    }\n}\n\nfunc main() {\n    ctx := context.Background()\n    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))\n\n    title, err := templates.ParseStringTemplate(\"title\", `{{ .Service }} is {{ .Status }}`)\n    if err != nil {\n        panic(err)\n    }\n\n    body, err := templates.ParseTemplate(\"webhook\", `{\"text\": {{ .Title | json }}}`, templates.WithDefaultFuncs())\n    if err != nil {\n        panic(err)\n    }\n\n    target := webhook.New(\n        webhook.WithURL(\"https://example.com/webhook\"),\n        webhook.WithTitleTemplate(title),\n        webhook.WithTemplate(body),\n        webhook.WithClient(webhook.NewClient(10*time.Second)),\n        webhook.WithLogger(logger),\n        webhook.WithValidateJSON(),\n    )\n\n    receiver := notify.NewReceiver(\"ops\", target).\n        WithRetry(notify.RetryConfig{\n            Count:   2,\n            Backoff: time.Second,\n        })\n\n    err = notify.SendTo(ctx, Alert{\n        IDValue: \"alert-1\",\n        Service: \"api\",\n        Status:  \"down\",\n    }, receiver)\n    if err != nil {\n        panic(err)\n    }\n}\n```\n\nRunnable examples are available in:\n\n```text\nexamples/single/      synchronous send to one receiver\nexamples/multiple/    synchronous send to multiple receivers\n```\n\nRun them with:\n\n```sh\ngo run ./examples/single\ngo run ./examples/multiple\n```\n\n## Receiver helpers\n\n`notify.NewReceiver` and the fluent receiver methods are convenience helpers for simple setup:\n\n```go\nreceiver := notify.NewReceiver(\"ops\", slackTarget, emailTarget).\n    WithName(\"Operations\").\n    WithCustomData(map[string]any{\"channel\": \"alerts\"}).\n    WithRetry(notify.RetryConfig{Count: 2, Backoff: time.Second})\n\nerr := notify.SendTo(ctx, alert, receiver)\n```\n\nAvailable helpers:\n\n```go\nfunc NewReceiver(id ReceiverID, targets ...Target) *Receiver\nfunc NewReceivers(receivers ...*Receiver) Receivers\nfunc SendTo(ctx context.Context, notification Notification, receivers ...*Receiver) error\n\nfunc (r *Receiver) WithName(name string) *Receiver\nfunc (r *Receiver) WithCustomData(customData map[string]any) *Receiver\nfunc (r *Receiver) WithRetry(cfg RetryConfig) *Receiver\nfunc (r *Receiver) WithTargets(targets ...Target) *Receiver\n```\n\n`SendTo` 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.\n\n## Target options\n\nWebhook and email targets use functional options for simple construction.\n\n```go\nclient := webhook.NewClient(\n    10*time.Second,\n    webhook.WithProxyFromEnvironment(),\n    webhook.WithSkipTLSVerify(),\n)\n\nwebhookTarget := webhook.New(\n    webhook.WithName(\"slack-alerts\"),\n    webhook.WithURL(\"https://example.com/webhook\"),\n    webhook.WithClient(client),\n    webhook.WithTitleTemplate(title),\n    webhook.WithTemplate(body),\n    webhook.WithValidateJSON(),\n)\n\nemailTarget := email.New(\n    email.WithHost(\"smtp.example.com\"),\n    email.WithPort(587),\n    email.WithCredentials(\"user\", \"pass\"),\n    email.WithFrom(\"alerts@example.com\"),\n    email.WithTo(\"ops@example.com\"),\n    email.WithCC(\"lead@example.com\"),\n    email.WithBCC(\"audit@example.com\"),\n    email.WithSubjectTemplate(subject),\n    email.WithTemplate(body),\n)\n```\n\n## Config-driven usage\n\nFor config-driven applications, use a `notify.Receivers` map directly. The map key is the receiver ID used for routing.\n\n```go\nreceivers := notify.Receivers{\n    \"ops\": {\n        Name: \"Operations\",\n        Retry: notify.RetryConfig{\n            Count:   2, // two retries, three total attempts\n            Backoff: time.Second,\n        },\n        CustomData: map[string]any{\n            \"channel\": \"alerts\",\n        },\n        Targets: []notify.Target{\n            slackWebhook,\n            emailTarget,\n        },\n    },\n}\n\nerr := notify.Send(ctx, alert, receivers, logger)\n```\n\nNotifykit normalizes receiver configuration when receivers enter `Send`, `SendTo`, `NewReceivers`, or `NewManager`:\n\n```text\nempty Receiver.ID      defaults to the receiver map key\nempty Receiver.Name    defaults to Receiver.ID\nnil receiver value     skipped during routing and delivery\n```\n\nNormalization is applied to internal receiver copies, so the receiver values passed by the caller are not modified.\n\n## Manager example\n\nFor queued asynchronous delivery, use `notify.NewManager`, start it once, and enqueue notifications over time.\n\n```go\nmanager, err := notify.NewManager(receivers, logger)\nif err != nil {\n    panic(err)\n}\nif err := manager.Start(ctx); err != nil {\n    panic(err)\n}\n\nqueueID, err := manager.Enqueue(ctx, alert)\nif err != nil {\n    panic(err)\n}\nfmt.Println(\"queued notification\", queueID)\n```\n\nManagers use one worker by default. Use `notify.WithWorkers` when queued notifications should be delivered concurrently:\n\n```go\nmanager, err := notify.NewManager(receivers, logger, notify.WithWorkers(4))\n```\n\nWhen more than one worker is configured, different notifications may call the same target at the same time. Built-in targets are safe for this; custom targets should also be safe for concurrent `Send` calls.\n\n## Flow\n\n```mermaid\nflowchart LR\n    A[Application event] --\u003e B[notify.Notification]\n    B --\u003e C{Usage style}\n    C --\u003e D[notify.SendTo]\n    C --\u003e E[notify.Send]\n    C --\u003e F[Manager.Enqueue]\n    F --\u003e G[internal store]\n    F --\u003e H[Mailbox]\n    H --\u003e I[internal dispatcher]\n    D --\u003e J[Build receiver map]\n    E --\u003e K[Resolve receivers]\n    I --\u003e K\n    J --\u003e K\n    K --\u003e L[internal delivery]\n    L --\u003e M[Retry]\n    M --\u003e N[Target]\n    N --\u003e O[Render title or subject]\n    O --\u003e P[Render body with title or subject]\n    P --\u003e Q[Send webhook or email]\n```\n\n## Packages\n\n```text\nnotify/             queue, dispatcher, receivers, retries, and target interfaces\ntemplates/          template loading, parsing, and rendering\ntargets/webhook/    HTTP webhook target\ntargets/email/      SMTP email target\n```\n\n## Notification contract\n\nApplications implement this interface:\n\n```go\ntype Notification interface {\n    ID() string\n    Data(receiver string, customData map[string]any, subject string) any\n}\n```\n\n`ID` returns a stable notification identifier for logs and delivery tracing.\n\n`Data` returns the template context. Targets call it twice: first with an empty string to render their title or subject template, then with the rendered value so the body template can reuse it. Webhook render data commonly exposes that value as `.Title`; email render data commonly exposes it as `.Subject`.\n\nTo select specific receivers, also implement `notify.ReceiverRouter`:\n\n```go\ntype ReceiverRouter interface {\n    ReceiverIDs() []ReceiverID\n}\n```\n\n`ReceiverIDs` controls routing. The returned values are matched against the keys in the receiver map passed to `notify.Send`, `notify.SendTo`, or `notify.NewManager`.\n\n```go\nfunc (a Alert) ReceiverIDs() []notify.ReceiverID {\n    return []notify.ReceiverID{\"ops\"}\n}\n```\n\nRouting behavior:\n\n```text\nnil or empty ReceiverIDs()        send to all configured receivers\n[]ReceiverID{\"ops\"}               send to receiver ID \"ops\"\n[]ReceiverID{\"ops\", \"dev\"}        send to both receiver IDs\nunknown receiver ID               skip that receiver and log a warning\n```\n\n## Receivers and targets\n\nA receiver groups one or more delivery targets and optional receiver-scoped settings.\n\n```go\nreceiver := notify.NewReceiver(\"ops\", slackWebhook, emailTarget).\n    WithName(\"Operations\").\n    WithCustomData(map[string]any{\"channel\": \"alerts\"}).\n    WithRetry(notify.RetryConfig{Count: 2, Backoff: time.Second})\n```\n\n`Receiver.ID` is the routing identifier. `Receiver.Name` is passed into the notification payload as the receiver name. When `ID` or `Name` is empty, Notifykit fills defaults from the receiver map key on internal copies during normalization.\n\n## Webhook target dependencies\n\nWebhook 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`.\n\nThis keeps `notify.Manager` transport-agnostic and still lets applications provide a custom `*http.Client` for timeouts, transports, proxies, tracing, mTLS, or tests.\n\n```go\nclient := webhook.NewClient(\n    5*time.Second,\n    webhook.WithProxyFromEnvironment(),\n)\ntarget := webhook.New(\n    webhook.WithURL(\"https://example.com/webhook\"),\n    webhook.WithTitleTemplate(title),\n    webhook.WithTemplate(body),\n    webhook.WithClient(client),\n    webhook.WithLogger(logger),\n    webhook.WithValidateJSON(),\n)\n```\n\nBy 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.\n\n## Email target recipients\n\nEmail targets support primary, CC, and BCC recipients.\n\n```go\ntarget := email.New(\n    email.WithHost(\"smtp.example.com\"),\n    email.WithFrom(\"alerts@example.com\"),\n    email.WithTo(\"ops@example.com\"),\n    email.WithCC(\"lead@example.com\"),\n    email.WithBCC(\"audit@example.com\"),\n    email.WithSubjectTemplate(subject),\n    email.WithTemplate(body),\n)\n```\n\n`To` and `CC` are written as message headers. `BCC` recipients are sent as SMTP envelope recipients but are not written to the message headers.\n\n## Templates\n\nTemplates use Go `text/template`. No helper functions are enabled by default; opt in to Notifykit's default helpers with `WithDefaultFuncs`.\n\n```go\nsubject, err := templates.ParseStringTemplate(\"subject\", `{{ .Service }} is {{ .Status }}`)\nbody, err := templates.LoadSource(templateFS, \"builtin:slack\", templates.WithDefaultFuncs())\n```\n\nMissing map keys fail by default. Use `WithMissingKey` for looser templates:\n\n```go\ntmpl, err := templates.ParseStringTemplate(\n    \"subject\",\n    `{{ .Service }}`,\n    templates.WithMissingKey(templates.MissingKeyDefault),\n)\n```\n\n`WithDefaultFuncs` enables the helper map returned by `DefaultFuncs`. Notifykit uses `github.com/containeroo/tmplfuncs` for these helpers, including `json`, `default`, `coalesce`, `formatTime`, `trim`, `upper`, `lower`, `withPrefix`, `withSuffix`, `optional`, `when`, and `duration`:\n\n```go\nbody, err := templates.ParseTemplate(\n    \"webhook\",\n    `{\"text\": {{ print\n        \"Expected every: \" (.ExpectedEvery | duration) \"\\n\"\n        \"Expected by: \" (.ExpectedBy | formatTime \"2006-01-02 15:04:05 MST\")\n        | json\n    }}}`,\n    templates.WithDefaultFuncs(),\n)\n```\n\nApplications can add or override project-specific template functions with `WithFunc` or `WithFuncs`.\nThis is useful for formatting that should be owned by the application.\n\n```go\nfunc formatDuration(d time.Duration) string {\n    if d \u003c 0 {\n        d = -d\n    }\n    if d \u003c time.Second {\n        return d.Truncate(time.Millisecond).String()\n    }\n    return d.Truncate(time.Second).String()\n}\n\nbody, err := templates.ParseTemplate(\n    \"webhook\",\n    `{\"text\": {{ (.Duration | formatDuration) | json }}}`,\n    templates.WithDefaultFuncs(),\n    templates.WithFunc(\"formatDuration\", formatDuration),\n)\n```\n\nFor complete control, build a function map yourself and pass only the helpers you want:\n\n```go\nfuncs := templates.DefaultFuncs()\ndelete(funcs, \"duration\")\nfuncs[\"formatDuration\"] = formatDuration\n\nbody, err := templates.ParseTemplate(\n    \"webhook\",\n    `{\"text\": {{ (.Duration | formatDuration) | json }}}`,\n    templates.WithFuncs(funcs),\n)\n```\n\n## Development\n\nRun the full local check suite with:\n\n```sh\nmake test\n```\n\nThis runs `go fmt`, `go vet`, and `go test -covermode=atomic` for all non-example packages.\n\n## Application boundary\n\nKeep these parts in your application:\n\n- config parsing\n- event types\n- render data structs\n- built-in template aliases and defaults\n- database persistence\n- metrics and audit logging\n\nNotifykit owns only the notification mechanics.\n\n## License\n\nThis project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontaineroo%2Fnotifykit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcontaineroo%2Fnotifykit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontaineroo%2Fnotifykit/lists"}