Files
suaveplan/openspec/archive/2026-05-19-add-notification-channel/proposal.md
2026-05-27 23:29:53 +01:00

6.4 KiB

Ship @suaveplan/notification-channel

Package: @suaveplan/notification-channel Status: PROPOSED Type: new-package (migration)

  • Design: ./design.md (inlined below — single-file change)
  • Spec delta: ./specs/notification-channel/spec.md

Why

Genesis needs a unified multi-channel notification router so downstream services (api-plugin-notifications, marketing-ops, alerting) have a single, deterministic surface for delivering messages over email, SMS, push, in-app, and webhooks. The upstream @suaveplan/notification-channel@1.2.6 ships in ../suaveplan/packages/universal/notification-channel/ with 34 source files and ~95% coverage; this change lands a feature-complete port at 100/100/100/100 coverage, rebuilt against the genesis foundation (defineCodes registry, EventBus, Zod boundaries, OTel meters, injectable FetchLike/Clock).

What changes

This is a brand-new package — every export is ADDED.

  • ADDED: NotificationRouter — primary entrypoint; routes notifications across channels with template rendering, sliding-window rate limiting, status lifecycle, EventBus emission, optional delivery-channel dispatch.
  • ADDED: EmailChannel, SmsChannel, PushChannel, InAppChannel, WebhookChannel, DigestChannel — six DeliveryChannel implementations.
  • ADDED: TemplateEngine — channel-specific template variants with {{var}} substitution.
  • ADDED: ChannelRateLimiter — sliding-window per-channel + per-user rate limits.
  • ADDED: DeliveryRetry — exponential-backoff retry wrapper with dead-letter queue.
  • ADDED: NotificationScheduler — schedule-for-future + interval processing.
  • ADDED: PreferenceManager — per-user opt-in/out + quiet-hours, storage-adapter pluggable.
  • ADDED: NOTIFICATION_CHANNEL_CODES — defineCodes("NOTIFICATION_CHANNEL", […]) registry with 11 stable reason codes.
  • ADDED: notificationHooks / onNotificationRouting — pre-routing hook pipeline.
  • ADDED: FakeFetch test double for FetchLike.
  • ADDED: OTel meters: notification_channel.routed, notification_channel.delivery_attempts, notification_channel.dead_letters, notification_channel.latency_ms.
  • ADDED: Zod schemas at every public option boundary.

Source mapping

This change derives from ../suaveplan/enterprise/notification-channel.md and the upstream package source.

Requirement Source lines Notes
Multi-channel router with EventBus + lifecycle ../suaveplan/packages/universal/notification-channel/src/router/notification-router.ts L1-L313 Re-shaped onto genesis EventBus; injectable Clock.
Email / SMS / Push / InApp / Webhook channels src/channels/*.ts (upstream) Webhook uses injectable FetchLike. Push uses injectable FetchLike for VAPID POST.
DigestChannel src/digest/digest-channel.ts Injectable Clock for interval gating.
TemplateEngine with channel variants src/templates/template-engine.ts Verbatim.
ChannelRateLimiter sliding-window src/rate-limiter/channel-rate-limiter.ts Injectable Clock.
DeliveryRetry + dead-letters src/retry/delivery-retry.ts Injectable Clock/TimersLike sleeper.
NotificationScheduler src/scheduler/notification-scheduler.ts Injectable Clock.
PreferenceManager + quiet hours src/preferences/preference-manager.ts Injectable Clock.
Error codes registry enterprise/notification-channel.md L51-L75 + upstream error-codes.ts + errors/notification-errors.ts Reunified onto defineCodes("NOTIFICATION_CHANNEL", [...]); the upstream split between error-codes.ts (process-wide ErrorCodes) and errors/notification-errors.ts (NotificationError factory) is collapsed into one stable code registry per genesis convention.
Hooks pipeline src/hooks.ts Reimplemented as local Hooks<Map> — @suaveplan/events does not yet export Hooks on genesis.

Requirements with no source line

Requirement Justification
OTel meters + refreshNotificationMeters Enterprise audit §3 requires OTel; upstream has none — added per genesis convention.
Zod boundary schemas Enterprise audit §3 requires Zod at every public type — upstream has none — added per genesis convention.
Injectable FetchLike in webhook/push Genesis universal-tier deterministic-fetch convention — upstream uses raw globalThis.fetch.
FakeFetch test double Replaces vi.mock/runtime-monkey-patching tests (no mocks rule).

Verification approach

Every requirement in the spec delta carries a **Verified by:** line naming concrete test titles.

  • src/error-codes.test.ts — registry stability, code shape, side-effect import.
  • src/channels/<channel>.test.ts — one per channel.
  • src/digest/digest-channel.test.ts — digest queueing/flushing.
  • src/router/notification-router.test.ts + src/router/notification-router-routing.test.ts — send/lifecycle/routing/rate-limit.
  • src/rate-limiter/channel-rate-limiter.test.ts, src/retry/delivery-retry.test.ts, src/scheduler/notification-scheduler.test.ts, src/templates/template-engine.test.ts, src/preferences/preference-manager.test.ts, src/hooks/hooks.test.ts, src/types/notification.types.test.ts.
  • src/otel/metrics.test.ts — instruments fire against real in-memory MeterProvider.
  • src/fakes/fake-fetch.test.ts — fake-fetch double.

Out of scope

  • Real provider integrations (Twilio, SendGrid, Amazon SES, Firebase Cloud Messaging) — gated by EmailChannel.sendFn/SmsChannel.sendFn/PushChannel.sendFn callbacks. Provider adapters land in sibling packages (@suaveplan/emailer, @suaveplan/sms-providers) per the genesis enterprise plan.
  • Persistence of notification history — the in-memory Map<string, Notification> is intentional; downstream consumers wire their own storage adapter through EventBus.on("notification.sent", …).

Excluded from this migration

None — the migration is at full functional parity with upstream 1.2.6 plus genesis enterprise enhancements (OTel, Zod boundaries, injectable fetch).

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/notification-channel to enumerate. This is a brand-new package on genesis — no genesis consumers yet.

  • @suaveplan/api-plugin-notifications — [x] no action needed (consumer will land in a follow-up change; no genesis code depends on this package today).

Migration impact

None — additive only.