Skip to content

feat(messaging): ADR-0030 P3a — email channel + notification templates#1449

Merged
os-zhuang merged 3 commits into
mainfrom
claude/adr-0030-p3
Jun 1, 2026
Merged

feat(messaging): ADR-0030 P3a — email channel + notification templates#1449
os-zhuang merged 3 commits into
mainfrom
claude/adr-0030-p3

Conversation

@os-zhuang
Copy link
Copy Markdown
Contributor

Summary

Implements ADR-0030 P3a — email channel + templates, continuing from merged P0 (#1434) / P1 (#1441) / P2 (#1444). The same emit() now reaches inbox + email per the user's preferences, rendered from a template — the build spec's P3 acceptance criterion.

P3 is split into slices for reviewability: P3a (this PR) = email channel + templates; P3b (next) = digest + quiet-hours.

What changed

email channel (createEmailChannel)

A thin MessagingChannel that delegates transport to the existing email service (ADR-0022: the channel adds messaging semantics; the email sub-system stays the transport — no hand-rolled SMTP, no new connector). On send() it:

  1. resolves the recipient user id → address (sys_user.email; an email-shaped recipient is used verbatim),
  2. renders (topic, 'email', locale) from a template (fallback to payload.title/body),
  3. hands subject + html/text to the email service.

Retry/backoff/dead-letter come free from the P1 outbox dispatcher. Registered at kernel:ready only when an email service is present (absent ⇒ no channel, so an explicit channels:['email'] reports "not registered" rather than silently no-opping). No-ops gracefully when the capability isn't installed, like the inbox channel.

sys_notification_template + renderer

  • Object keyed by (topic, channel, locale) with subject/body/format.
  • Declarative {{ payload.x }} interpolation — no logic/conditionals (templates stay auditable metadata, a deliberate low-code constraint); unknown path → empty string; linear-time single pass.
  • HTML/markdown/text bodies; locale fallback en-US → en → default.
  • Generic fallback to payload.title/body when no template matches → templates are purely additive (P0/P1 behavior preserved).
  • Contributed to the Setup → Configuration nav.

Architecture note (answers "should Slack be a connector?")

Per ADR-0022: Slack stays a connectorconnector-slack already ships the raw chat.postMessage path, which covers destination-explicit "post to #ops". A Slack notification channel (per-user DM honoring prefs + threads) needs identity mapping (sys_channel_user_link) + OAuth and is enterprise-tier — deferred. When built, its send() delegates to the existing connector. push (needs sys_user_device + APNs/FCM) and webhook (reuse plugin-webhooks outbox) are likewise deferred on the same seam — P1 gives any new channel retry/outbox for free.

Acceptance

  • ✅ Same emit reaches inbox + email per prefs, email rendered from a template (generic fallback when none).

Testing

service-messaging: 85 passing — adds template-renderer.test.ts (interpolation, html/text/fallback rendering, locale fallback, lookup-error → null) and email-channel.test.ts (address resolution, template vs fallback, no-service no-op, unresolved-address failure, transport-failure retry). service-automation 105 ✓.

Follow-ups

  • P3b: digest + quiet-hours as deferred next_attempt_at in the P1 outbox (one delivery spine, not a parallel scheduler); consumes P2's digest/quiet_hours fields; critical/mandatory bypass.
  • MJML compilation (P3a treats mjml format as raw HTML); defineTopic() topic catalog; push/webhook/Slack channels.
  • Still pending from P0: objectui bell cut-over + mark-read receipt write.

🤖 Generated with Claude Code


Generated by Claude Code

The same emit() now reaches inbox AND email per the user's preferences,
rendered from a template.

- email channel (createEmailChannel): a thin MessagingChannel that delegates
  transport to the existing `email` service (ADR-0022). Resolves recipient
  user id → sys_user.email (or an email-shaped recipient verbatim), renders,
  sends. Retry/backoff/dead-letter come from the P1 outbox dispatcher.
  Registered at kernel:ready only when an `email` service is present.
- sys_notification_template (topic × channel × locale) + renderer: declarative
  {{ payload.x }} interpolation (no logic — auditable), html/markdown/text
  bodies, locale fallback (en-US → en → default), generic fallback to
  payload.title/body when no template. Contributed to Setup → Configuration nav.

Scope (ADR-0022): Slack stays a connector (connector-slack already ships the
raw API path); a Slack notification channel needs identity mapping + OAuth and
is enterprise-tier — deferred. push/webhook channels + digest/quiet-hours (P3b)
are follow-ups on the same seam.

Tests: service-messaging 85 passing (adds template-renderer.test.ts +
email-channel.test.ts).

https://claude.ai/code/session_015pRGvrm3zrk5m8YvZhkAmF
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 1, 2026 6:15am

Request Review

@os-zhuang os-zhuang marked this pull request as ready for review June 1, 2026 05:49
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Jun 1, 2026
@os-zhuang os-zhuang merged commit f3424fc into main Jun 1, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants