feat(messaging): ADR-0030 P3a — email channel + notification templates#1449
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…-r build + turbo test both green locally) https://claude.ai/code/session_015pRGvrm3zrk5m8YvZhkAmF
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
emailchannel (createEmailChannel)A thin
MessagingChannelthat delegates transport to the existingemailservice (ADR-0022: the channel adds messaging semantics; the email sub-system stays the transport — no hand-rolled SMTP, no new connector). Onsend()it:sys_user.email; an email-shaped recipient is used verbatim),(topic, 'email', locale)from a template (fallback topayload.title/body),Retry/backoff/dead-letter come free from the P1 outbox dispatcher. Registered at
kernel:readyonly when anemailservice is present (absent ⇒ no channel, so an explicitchannels:['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(topic, channel, locale)withsubject/body/format.{{ payload.x }}interpolation — no logic/conditionals (templates stay auditable metadata, a deliberate low-code constraint); unknown path → empty string; linear-time single pass.en-US → en → default.payload.title/bodywhen no template matches → templates are purely additive (P0/P1 behavior preserved).Architecture note (answers "should Slack be a connector?")
Per ADR-0022: Slack stays a connector —
connector-slackalready ships the rawchat.postMessagepath, 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, itssend()delegates to the existing connector. push (needssys_user_device+ APNs/FCM) and webhook (reuseplugin-webhooksoutbox) are likewise deferred on the same seam — P1 gives any new channel retry/outbox for free.Acceptance
emitreaches 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) andemail-channel.test.ts(address resolution, template vs fallback, no-service no-op, unresolved-address failure, transport-failure retry). service-automation 105 ✓.Follow-ups
next_attempt_atin the P1 outbox (one delivery spine, not a parallel scheduler); consumes P2'sdigest/quiet_hoursfields; critical/mandatory bypass.mjmlformat as raw HTML);defineTopic()topic catalog; push/webhook/Slack channels.🤖 Generated with Claude Code
Generated by Claude Code