Comms Registry¶
The authoritative record of every outbound interaction the system sends — email today, SMS and other channels as they are added. One entry per message type, owned by the module whose event triggers it.
Why this document exists¶
Outbound interactions ramp up fast and become unmanageable without a single source of truth. The frustration this prevents: facing a large set of system-generated messages and not knowing which is which, what triggers it, who it reaches, or what's wrong with it. This register is that source of truth — durable, version-controlled canon, independent of whether any UI surface for it has been built yet.
THE COMMS-ENTRY RULE (governing)¶
No outbound interaction ships without an entry in this register, under its owning module. Same discipline as the EXT-API rule (any external API needs a reference doc before deploy). A new email or SMS gets a register entry — owning module, template key, trigger, channel, recipient, state — or it does not ship. The register is a gate, not a passive catalogue.
How the register is organised¶
Ownership by module. Each message belongs to the module whose event triggers it (the invite email belongs to People because it fires off the People onboarding flow). This mirrors the module structure and matches how a person hunts: "an onboarding email is wrong" → People section → its messages. Ownership also fixes responsibility: a module owns its own outbound voice — the wording and the trigger logic are that module's to get right.
Per-message detail. Each entry reads top-down so you can see what fires it and what's
in it at a glance:
- surface — the app / tile / worker the send originates from, and the route or event site
(e.g. apps/workspace · /admin/team). This is "which thing triggered it."
- trigger — the event that fires it.
- what it does — a one-line description of the message and its contents.
- template_key — stable internal identity (e.g. people.invite-welcome). The thing the
future template editor lists messages by. Set at creation, never reused.
- channel — email | sms | (multi). Channel-agnostic by design.
- recipient — who it goes to (prod / dev), and the intended recipient where they differ.
- routing — seam (through the ADR-046 sendTouchpoint seam: central copy + an ADR-045
record-of-send) or hand-rolled (a direct sendMail at the call site, not yet seam-routed).
- state — live | flawed | gap | planned | retired.
- findings — known defects, cross-referenced to the findings report.
The dev-recipient override (read once, applies throughout). Every person-facing send
overrides the recipient in dev: env.ENV === "dev" ? "client@rtopacks.dev" : <real address>.
CF Email Routing only delivers to verified destinations, so dev sends are sunk to one verified
address. A dev email landing at client@rtopacks.dev is by design, not a mis-route —
prod sends to the real person. Internal-ops sends (contact/lead/subscriber-notice, sync/archiver/
anomaly alerts) go to the ops address in all environments.
Population¶
Seeded 2026-06-23 from the approvals-arc walk, then fully enumerated the same day from the
codebase (COMMS-REGISTRY-EMAIL-SEND-ENUMERATION-01): every sendMail / sendTouchpoint /
EMAIL.send call site read and cross-checked against the in-code TOUCHPOINTS table
(workers/internal-api/src/mail-seam.ts). Result: 16 email send paths, all already declared —
zero orphan email sends — plus untracked SMS. As of COMMS-SEAM-ROUTING-SWEEP-01 (+ the born-routed
onboarding_approved), 12 sends are routed through the seam; 5 remain hand-rolled (Group C ops
diagnostics — deliberately not routed). Sections below are populated, not stubs.
People¶
Owns: onboarding / entry / approvals / grant-lifecycle messages.
Surfaces: apps/workspace (/admin/team, /entry, /admin/approvals); sent by internal-api.
people.invite-welcome — email¶
- surface: apps/workspace ·
/admin/team→ "Invite member" (sent by internal-apiorgs.tsinviteMember) - trigger: an admin invites a team member
- what it does: tells the invitee they've been invited (as team member or administrator) and points them to sign in
- template_key:
org_member_invite(seam) - channel: email · routing: seam (audited)
- recipient: invitee email (prod) /
client@rtopacks.dev(dev) - state: flawed
- findings:
- A1 / INVITE-EMAIL-LINK-GENERIC-01 (HIGH): links to bare
{workspace}/auth, forcing the invitee to re-type their email to trigger a second (magic-link) email. Fix: one-click link carrying the email straight into magic-link generation. (confirmed in the resolver bytes) - A2 / INVITE-EMAIL-THIN-01 (MED): no inviting-org identity, no purpose statement, no trust
framing. The custody line exists downstream (
/entry) but not in this email.
people.return-notification — email¶
- surface: apps/workspace ·
/admin/approvals→ return a section (sent by internal-apionboarding.ts:returnOnboarding+commitSectionReviews) - trigger: an admin returns a section for correction (commits a return verdict with a reason)
- what it does: tells the person their submission needs a change, states the reason, and points them to sign in and resubmit
- template_key:
onboarding_returned(seam) - channel: email · routing: seam (audited)
- recipient: invitee email (prod) /
client@rtopacks.dev(dev) - state: flawed
- findings:
- A3 (RESOLVED) / wrong-recipient was a DEV ARTEFACT: the walk saw it land at
client@rtopacks.dev— that is the dev override (above), not a mis-route. In prod it goes to the invitee. The callback exists and fires on return (it was briefly mis-logged "missing" in the walk; it had fired, just delivered late). - but: same bare
/authdead-end link as A1, and thin like A2 (no org identity).
people.magic-link-signin — email¶
- surface: apps/workspace ·
/auth(sent byapps/workspace/.../auth/magic-link/route.tsvia the seam) - trigger: a person requests sign-in (enters their email at
/auth) - what it does: sends the one-time sign-in link (expires 15 min, single-use)
- template_key:
magic_link_login(seam) · logged under People as it serves onboarding; generic auth co-owned with Platform/Auth - channel: email · routing: seam
- recipient: the email entered at
/auth - state: live — works cleanly. The one comms touch the walk found sound. (It is the second email in the broken invite flow; fixing A1 reduces how often a person triggers it manually.)
people.approval-grant-issued — email¶
- surface: apps/workspace ·
/admin/approvals→ "Approve & open grant" (sent by internal-apionboarding.tsapproveOnboarding) - trigger: co-sign approves the record and the T4A grant mints
- what it does: tells the person their onboarding is approved and their access is active, names the org ("Welcome to {org}"), and links to the workspace home (clean entry — NOT bare
/auth) - template_key:
onboarding_approved(seam) - channel: email · routing: seam (audited)
- recipient: approved person (prod) /
client@rtopacks.dev(dev) - state: live (PEOPLE-APPROVAL-GRANT-EMAIL-01, dev — prod at run-close) — the positive bookend to the
return email. Non-blocking (a send failure never fails the approval; the grant already minted). Org
name is best-effort (generic "RTOpacks" fallback if the
orgslookup misses). Born routed through the seam.
Platform / Auth & Account¶
Owns: admin-app sign-in/security mail and org-provisioning welcomes.
Surfaces: apps/admin; internal-api provisionOrg.
platform.admin-magic-link — email¶
- surface: apps/admin ·
/auth(apps/admin/.../auth/magic-link/route.ts) - trigger: an admin requests sign-in to the admin app
- what it does: sends the admin sign-in link (expires 15 min, single-use)
- template_key:
admin_magic_link· routing: seam (COMMS-SEAM-ROUTING-SWEEP-01; login → no audit) - recipient: admin email (prod) /
client@rtopacks.dev(dev) · state: live
platform.admin-added — email¶
- surface: apps/admin · administrators surface (
apps/admin/.../administrators/route.ts) - trigger: an administrator is added
- what it does: tells the new admin they've been added and points them to sign in
- template_key:
admin_added· routing: seam (audited) (COMMS-SEAM-ROUTING-SWEEP-01) - recipient: new admin (prod) /
client@rtopacks.dev(dev) · state: flawed (same bare/authas A1)
account.org-admin-welcome — email¶
- surface: internal-api
orgs.tsprovisionOrg(org provisioning) - trigger: a new client org is provisioned
- what it does: welcomes the first administrator and points them to set up their organisation
- template_key:
org_admin_welcome(seam, audited) · routing: seam - recipient: first admin (prod) /
client@rtopacks.dev(dev) · state: live (same bare/authpattern)
Site / Marketing¶
Owns: public marketing-site and pre-launch messages (apps/site, workers/prelaunch). Not a workspace product module — the public funnel.
site.subscribe-confirm — email¶
- surface: apps/site ·
/api/subscribe· trigger: newsletter subscribe (to the subscriber) - what it does: confirms the subscription ("you're on the list… no noise, no spam")
- template_key:
site_subscribe_confirm· routing: seam (audited) · recipient: subscriber /client@rtopacks.dev(dev) · state: live
site.subscriber-notice — email¶
- surface: apps/site ·
/api/subscribe· trigger: newsletter subscribe (to ops) - what it does: notifies ops of a new subscriber (name/email/phone/org)
- template_key:
site_subscriber_notice· routing: seam · recipient: ops (adminEmail, all envs) · state: live
site.subscribe-sms — SMS¶
- surface: apps/site ·
/api/subscribe(Twilio) · trigger: newsletter subscribe (to ops) - what it does: SMS-notifies ops of a new subscriber (no-op in staging)
- template_key: (none — inline Twilio) · channel: sms · routing: hand-rolled
- recipient: ops (Twilio) · state: live — the register's first non-email channel; was untracked until the enumeration.
site.contact — email¶
- surface: apps/site ·
/api/contact· trigger: contact form submitted - what it does: sends ops the enquiry (name/org/email/type/message + IP/UA)
- template_key:
site_contact· routing: seam · recipient: ops (adminEmail) · state: live · (also fires a Twilio SMS — see SMS note)
site.lead — email¶
- surface: apps/site ·
/api/lead· trigger: early-access lead captured - what it does: sends ops the lead (email + signup detail)
- template_key:
site_lead· routing: seam · recipient: ops (adminEmail) · state: live
site.prelaunch-waitlist — email¶
- surface: workers/prelaunch · trigger: waitlist signup
- what it does: asks the signup to confirm their waitlist spot
- template_key:
prelaunch_waitlist· routing: hand-rolled · recipient: waitlist signup · state: live
Intake Pipeline (shared infrastructure)¶
Owns: the shared candidate-intake channel (invitation + reminder + advice delivery). Not a workspace product module — shared infrastructure consumed by LLND (and future intake tiles). The content-owning tile is named per entry.
intake.invitation — email¶
- surface: apps/workspace · POST /api/llnd/invitations (+ /reissue)
- trigger: an operator (T4) issues or re-issues an LLND invitation
- what it does: sends the tokened self-complete link (expires 14 days, one live token)
- template_key: intake_invitation · routing: seam · audit: true (ADR-045 — the ev1-attached send record is delivery evidence)
- recipient: candidate address (prod) / client@rtopacks.dev (dev) · state: live
- findings: tokened link only — no name/USI/identifiers in the URL
llnd.advice-delivery — email¶
- surface: apps/workspace · POST /api/llnd/reviews/:id/advice/deliver
- trigger: a human-issued advice is delivered down the invitation's channel
- what it does: delivers the advice to the candidate as inline body text (D-1 option b)
- template_key: llnd_advice_delivery · routing: seam · audit: true · content-owning tile: LLND
- recipient: candidate address (prod) / client@rtopacks.dev (dev) · state: live
- findings: body carries the human-issued verdict + support/alternatives content ONLY — per-capability profile detail never rides the email; delivery_record carries a reference (advice.id), never content; read-back ruled out; attachment deferred
intake.reminder — email¶
- template_key: intake_reminder · state: PLANNED (not built this brief) · cadence deferred with it
Operations / Observability¶
Owns: internal system alerts (not user-facing). Surfaces: internal-api + sync/archiver workers. Recipient is always internal ops, in all environments.
ops.anomaly-session-revoked(anomaly_session_revoked) — internal-apianomaly.ts; fires on a SEC-02 anomaly (session revoked); alerts ops/security. seam (COMMS-SEAM-ROUTING-SWEEP-01) · live.ops.archiver-failure(archiver_failure) — workers/session-archiver; fires when a session archive fails; alerts ops. hand-rolled · live.ops.cricos-sync-abort(cricos_sync_abort) — workers/cricos-sync; fires when cricos-sync aborts; alerts ops. hand-rolled · live.ops.cricos-sync-report(cricos_sync_report) — workers/cricos-sync; fires on run complete; run summary to ops. hand-rolled · live.ops.enrich-sync-report(enrich_sync_report) — workers/enrich-sync; fires on run complete; run summary to ops. hand-rolled · live.
Studio · Record · Radar / Field Observer · InstaLearn · Billing / Account¶
No outbound interactions today (confirmed by enumeration — these modules send nothing). When the first send is added to any of them, it gets an entry here under its owning module before it ships (COMMS-ENTRY rule). Billing note: billing-state changes drive deactivate/reactivate (see lifecycle doc); the first billing→member email belongs here and interacts with the seat/lifecycle work.
Enumeration summary (2026-06-23)¶
- 16 email sends found at enumeration (17 now, +
onboarding_approved), all declared in the in-codeTOUCHPOINTSregistry — zero orphan email sends. - 12 routed through the ADR-046 seam — the People/auth set (
magic_link_login,org_member_invite,org_admin_welcome,onboarding_returned,onboarding_approved) + the COMMS-SEAM-ROUTING-SWEEP-01 set (admin_magic_link,admin_added,site_subscribe_confirm,site_subscriber_notice,site_contact,site_lead,anomaly_session_revoked). 5 hand-rolled remain — Group C ops diagnostics (prelaunch_waitlist,cricos_sync_abort,cricos_sync_report,enrich_sync_report,archiver_failure), deliberately not routed (internal-only; routing them would need INTERNAL_API bindings on the sync workers). - 2 untracked SMS (site subscribe + site contact → ops, via Twilio) — the SMS channel is not yet in the register's per-message model; decision owed at the comms cluster.
- Gap closed:
people.approval-grant-issuedis now live (PEOPLE-APPROVAL-GRANT-EMAIL-01) —onboarding_approved, born routed. - Resolved walk finding:
people.return-notificationwrong-recipient = dev artefact (prod → invitee).
Standing notes (for the comms cluster / template editor, not now)¶
- Traceability is AUDIT-LOG-ONLY (live). Every audited routed send writes an ADR-045
correspondence_sentrow (template_key + recipient + time) — so "what did we send, to whom, when, which template" is answerable now, from the audit log. That is the live trace mechanism. - Message-side header trace — PARKED, not viable by design (COMMS-HEADER-TRACEABILITY-01, validate-first
close 2026-06-23). The plan was to stamp
X-RTOpacks-Template/X-RTOpacks-Ref(= the ADR-045 row id) into a custom email header. Butenv.EMAILis the CF-nativesend_emailbinding: the caller passes a fixed structured object with noheadersfield and CF builds the MIME server-side — there is no point to attach a custom header without switching to raw-MIME construction (a substrate change across all 10mail.tscopies, not worth it pre-revenue). Banked: the stamp design + the X-RTOpacks-Ref=row-id decision are ready, and the seam is the one-place add — IF headers ever become viable (future CF capability or a raw-MIME adoption). Seeoutputs/COMMS-HEADER-TRACEABILITY-01-gate1-VALIDATE.md. - Seam-routing sweep: DONE for the customer/admin-facing sends (COMMS-SEAM-ROUTING-SWEEP-01 — Group A+B, 7 sends). The remaining 5 Group C ops-diagnostic sends stay hand-rolled (deliberate cut). Route them later only if a reason emerges (needs INTERNAL_API bindings on the sync workers).
- Template editor (future): lists messages by human name +
template_key; lets each module author its own wording. This register's keys are the foundation the editor hangs on. - SMS: arrives as an added
channelon existing entries (the register's shape does not change);site.subscribe-smsis the first live instance. Multi-channel messages showchannel: email + sms. - The Notifications tile (admin · OPERATIONS, currently "Coming soon") is the eventual UI surface over this register and the end-user notification centre — Sentinel territory. This document remains the master regardless of the tile's state.
Comms Registry — canon. Seeded from the approvals walk; fully enumerated from code 2026-06-23. The COMMS-ENTRY rule governs: no outbound ships without an entry under its owning module.