This page documents every non-Telegram messaging surface V1 ships: WhatsApp
Business, email, push (FCM, APNs, Expo, and W3C Web Push), SMS, Discord, and
Slack. It serves customers (transactional and grounded delivery on whichever
channel they have opted into), tenants and institutions (residency-aware
allowlists, per-message cost ledgers, audit), and operators (deliverability
monitoring and a credential→transport seam that fails loud instead of faking a
send). All of it lives in one Nx library, @oshun/messaging-channels
(libs/oshun/messaging-channels/src), and it sits beneath the shared routing
spine described in
Channel Abstraction, Routing, and Boundaries;
its sibling surface page is Telegram Surfaces.
The single rule that shapes everything here is the same rule the channel
abstraction enforces: no aaa-creator or operator-admin intent ever reaches
a messaging channel, and nothing is reported as "sent" unless a real provider
transport actually sent it. Where a transport is a pure planner that still
needs live credentials, this page says so explicitly rather than implying a
shipped integration.
Where this lives in V1#
Each channel is implemented as a small, pure-data adapter (the planner / payload-builder), plus a real provider transport that the credential seam wires in only when every credential is present:
| Channel | Adapter (planner / payload) | Real transport (HTTP / protocol) |
|---|---|---|
whatsapp/index.ts (planWhatsAppTemplateDelivery) |
sendWhatsAppViaMetaCloud (transports.ts) |
|
email/index.ts (buildTransactionalEmail) |
sendEmailViaSendgrid (transports.ts), sendEmailViaSmtp (smtp-email-transport.ts) |
|
| Push (FCM) | push/index.ts (buildPushPayload) |
sendPushViaFcm (transports.ts) |
| Push (APNs) | push/index.ts |
sendPushViaApns + buildApnsProviderJwt (transports.ts) |
| Push (Expo) | push/index.ts |
sendPushViaExpo (transports.ts) |
| Push (Web Push) | push/index.ts |
sendPushViaWebPush (web-push-transport.ts) |
| SMS | sms/index.ts (planSmsDelivery) |
sendSmsViaTwilio (transports.ts) |
| Slack | slack/index.ts (renderSlackOshunCommand) |
sendSlackViaWebApi (transports.ts) |
| Discord | discord/index.ts (renderDiscordAskCommand) |
sendDiscordViaBot (transports.ts) |
The seam between deploy-time secrets and these transports is
provider-config-env.ts; the registry that classifies every channel is
registry.ts; the routing decision that picks one of them is dispatcher.ts;
and the per-channel content-class and audit rules are boundary.ts. The
companion engineering reference is
../architecture/messaging-channels.md.
Accuracy note (architecture pointer). Earlier architecture text located the email/push/SMS adapters in
libs/shared/inbound-integrations/. The real outbound messaging adapters live inlibs/oshun/messaging-channels/src/{email,push,sms}andtransports.ts.libs/shared/inbound-integrationsis the inbound-connector and payment side, not the outbound transport layer.
The twelve registered channels#
The prose summary historically named a narrower set — "SMS (Twilio)" and "push
(FCM/APNs)". The registry has broadened well past that. registry.ts enumerates
twelve concrete channel IDs in CHANNEL_IDS:
telegram-bot, telegram-channel, telegram-miniapp,
whatsapp-business, discord-bot, slack-app, sms-twilio,
push-fcm, push-apns, push-expo, push-webpush, email-ses
Each ID carries a frozen ChannelEntry describing what the channel can do and
how it must be governed. The taxonomy the dispatcher reads is:
| Dimension | Constant | Values |
|---|---|---|
| Capabilities | CAPABILITY_KINDS |
text, rich-cards, inline-buttons, voice, files, payments, miniapp |
| Cost class | COST_CLASSES |
free, low, metered, high |
| Residency | RESIDENCY_PROFILES |
global, eu-only, us-only, apac-only, self-hosted |
| Consent | CONSENT_PROFILES |
platform-tos-implied, verified-opt-in-required, double-opt-in-required |
| Retention | RETENTION_PROFILES |
channel-default, short-30d, standard-365d, long-3y, indefinite |
Each non-Telegram channel's frozen registry row (from CHANNEL_REGISTRY):
| Channel | Capabilities | Cost | Consent | Retention | Crisis-capable |
|---|---|---|---|---|---|
whatsapp-business |
text, rich-cards, inline-buttons, voice, files | metered |
double-opt-in-required |
short-30d |
yes |
discord-bot |
text, rich-cards, inline-buttons, voice, files | free |
platform-tos-implied |
channel-default |
no |
slack-app |
text, rich-cards, inline-buttons, files | free |
platform-tos-implied |
channel-default |
no |
sms-twilio |
text | metered |
double-opt-in-required |
short-30d |
yes |
push-fcm |
text, rich-cards, inline-buttons | free |
verified-opt-in-required |
short-30d |
no |
push-apns |
text, rich-cards, inline-buttons | free |
verified-opt-in-required |
short-30d |
no |
push-expo |
text, rich-cards, inline-buttons | free |
verified-opt-in-required |
short-30d |
no |
push-webpush |
text, rich-cards, inline-buttons | free |
verified-opt-in-required |
short-30d |
no |
email-ses |
text, rich-cards, files | low |
verified-opt-in-required |
standard-365d |
no |
crisisCapable is the property that lets the dispatcher route a
crisis-hotline intent to WhatsApp or SMS but never to a push platform or a
community surface. disclosureRenderable records whether the channel can carry
visible disclosure copy in the message body (true for WhatsApp/email; false for
SMS and push, where disclosure rides in payload metadata).
Naming artifact. The email channel id is
email-ses, but there is no dedicated SES API transport in the lib — the real transports aresendEmailViaSendgrid(SendGrid HTTPS API) andsendEmailViaSmtp(a hand-written RFC 5321 SMTP client that also speaks to AWS SES SMTP, Postmark SMTP, or the dev-stack Mailpit sink). DKIM/SPF/DMARC are deploy-config concerns, not code in this lib.
How an outbound message is routed and sent#
A send is a two-stage pipeline: a pure routing decision followed by a real transport call wired in only when credentials exist.
- Route —
dispatchMessage(input)(dispatcher.ts) is a pure function fromDispatchInputtoDispatchDecision. It enforces, in order: the tier guard, crisis suppression, quiet hours, frequency caps, a non-empty tenant allowlist, and then per-channel opt-in / residency / crisis-capability / content-class / capability checks. It returns either{ kind: 'dispatch', channel, contentClass, chosenVariant }or{ kind: 'suppress', reason }with a typed reason such astier-not-allowed-on-messaging,no-opt-in, orcontent-class-not-allowed. - Send —
deliverWithEnvProviders({ decision, recipient, ... })(provider-config-env.ts) builds the provider config from the environment and callsdeliverDispatchedMessage, which invokes the channel's real transport. If the chosen channel's credentials are not present, it returnsmissing-config; if the recipient lacks the needed address, it returnsmissing-recipient; a non-dispatch decision returnssuppressed. None of these is a fabricated success.
The tier and intent model#
The dispatcher's TIERS are
['contemplative', 'curated-creator', 'aaa-creator', 'operator-admin'], but
only the first two are in MESSAGING_ALLOWED_TIERS. Any aaa-creator or
operator-admin intent is suppressed with tier-not-allowed-on-messaging
before any channel is even considered — this is the concrete enforcement of
"tier-aware routing." Intents are typed by INTENT_KINDS:
transactional-receipt, crisis-hotline, reminder,
session-notification, newsletter, engagement-recap
Each intent maps to a ContentClass via INTENT_CONTENT_CLASSES (for example
reminder → ritual-reminder, newsletter → editorial-briefing,
engagement-recap → grounded-answer), and each channel boundary lists which
content classes it will accept. crisis-hotline is the only intent allowed to
bypass crisis suppression, quiet hours, and frequency caps, and it must route
only to a crisisCapable channel.
Each message arrives as one or more ContentVariant objects, each carrying a
requiredCapability, bodyTextPlain, disclosureCopy, and provenanceFooter.
chooseVariantForChannel picks the first variant whose capability the channel
supports, falling back to a text variant so a rich-card message still degrades
to plain text on SMS rather than being dropped.
WhatsApp Business — transactional template messaging#
WhatsApp delivery is template-only and Meta-compliant. WhatsAppUseCase is the
allowed set:
receipt, reminder, password-reset, login-code, billing-alert, scheduled-event-nudge
planWhatsAppTemplateDelivery(templates, request) is pure-data so it is
testable without a Meta account. Its rules:
- It throws unless
optInActiveis true (Meta requires opt-in for every outbound). - It enforces the 24-hour customer-initiated session window
(
SESSION_WINDOW_SECONDS = 24 * 60 * 60). The window is open only when acustomerInitiatedAtUnixSecondsis present and within 24h ofnow. - It looks for an approved template matching name, locale, and use-case
(
findApprovedTemplate, which requiresapproved === true). If none exists, it returns an email-fallback plan —no-approved-templatewhen the window is open,session-window-closed-no-templatewhen it is closed — and throws if noemailFallbackAddresswas supplied. - On success it returns the chosen template plus a
CostLedgerEntry(channel: 'whatsapp-business', locale,amountMinor, use-case) bound to tenant cost controls.
ingestWhatsAppStatusWebhook normalizes Meta's delivery webhook
(sent | delivered | read | failed | opted_out) into a tenant-scoped key. The
real provider call, sendWhatsAppViaMetaCloud, POSTs to
https://graph.facebook.com/v21.0/<phoneNumberId>/messages with
messaging_product: 'whatsapp', type: 'template', and the body parameters; it
reads the returned messages[0].id and returns { ok: false } on any non-2xx.
The planner is real; sending requires live Meta Cloud API credentials.
Email — SendGrid HTTPS or a hand-written SMTP client#
buildTransactionalEmail builds the deliverable plan with a hard DKIM/SPF/DMARC
gate: it throws unless the tenant config has dkimVerified, spfVerified,
and a dmarcPolicy that is not none. The plan attaches List-Unsubscribe and
List-Unsubscribe-Post: List-Unsubscribe=One-Click headers plus
X-Oshun-Locale and X-Oshun-Tenant, and concatenates the disclosure copy and
provenance footer onto the body.
Inbound deliverability events flow through ingestEmailWebhookEvent
(sent | bounce | complaint | unsubscribe | open | click): a permanent bounce,
a complaint, or an unsubscribe suppresses the recipient with a typed
EmailSuppressionReason (hard-bounce, spam-complaint, list-unsubscribe,
manual-block); isEmailDeliverable then checks the suppression list before
the next send.
Two real transports back the email channel:
sendEmailViaSendgrid— POSTs tohttps://api.sendgrid.com/v3/mail/sendwith the SendGrid v3 shape (personalizations,from,subject,content) and reads thex-message-idresponse header. Returns{ ok: false }on any non-2xx.sendEmailViaSmtp(smtp-email-transport.ts) — a dependency-free RFC 5321 SMTP client overnode:net/node:tls. It speaks the protocol directly: greeting →EHLO→ optionalSTARTTLS→ optionalAUTH(AUTH PLAINif advertised, elseAUTH LOGIN, failing loud if neither) →MAIL FROM→RCPT TO→DATA→QUIT. It builds an RFC 5322 message (stripping CR/LF from injected header values to prevent header injection), dot-stuffs the body per RFC 5321 §4.5.2, and returns{ ok: false }with the failing status on any non-2xx reply, dropped connection, or timeout — never a fabricated success. The socket factory and the STARTTLS upgrade are injectable, so the full state machine is unit-tested against an in-process SMTP server with real sockets. This is the transport used for AWS SES SMTP, Postmark SMTP, a corporate relay, and the dev-stack Mailpit sink (:1025).
The credential seam (provider-config-env.ts) prefers SMTP when
OSHUN_SMTP_HOST is set, otherwise falls back to SendGrid when
OSHUN_SENDGRID_API_KEY is set; OSHUN_MESSAGING_EMAIL_FROM is required either
way.
Push — FCM, APNs, Expo, and W3C Web Push#
The push surface is wider than the historical "FCM (Android/web) and APNs (iOS)"
description. The payload builder, buildPushPayload (push/index.ts), still
synthesizes the three core platform shapes named in PushPlatform
(fcm-android, fcm-web, apns-ios). That covers silent background-sync
pushes (which may not expose user-visible actions), badge management,
action-button categories, deep-link click-actions, and thread IDs. But
CHANNEL_IDS additionally registers push-expo and push-webpush, each with
its own real transport:
sendPushViaFcm— POSTs tohttps://fcm.googleapis.com/v1/projects/<projectId>/messages:send(FCM HTTP v1) and reads the returnedname.sendPushViaApns— POSTs tohttps://api.push.apple.com/3/device/<token>(or the sandbox host) over what must be an HTTP/2 transport. It builds a real ES256 provider JWT withbuildApnsProviderJwt(P-256, IEEE-P1363r‖ssignature,{ alg:'ES256', kid }header), setsapns-topic/apns-push-type, and reads theapns-idheader. Because Node's globalfetchis HTTP/1.1,apnsHttp2BootWarningemits a startup warning when APNs is configured but no HTTP/2fetchImpl(createHttp2TransportFetch) is wired in.sendPushViaExpo— POSTs tohttps://exp.host/--/api/v2/push/sendfor React Native / ExpoExponentPushToken[...]clients. It inspects the returned Expo ticket and reports anerrorticket (e.g.DeviceNotRegistered) as a failure — never a faked success.sendPushViaWebPush(web-push-transport.ts) — the full W3C Web Push Protocol for PWAs. See the callout below.
Web Push is real, RFC-grade cryptography#
web-push-transport.ts implements the Web Push stack end-to-end: RFC 8030
(protocol), aes128gcm payload encryption per RFC 8291 + RFC 8188, and VAPID
application-server identification per RFC 8292. Because the lib is
client-transpiled (so node:crypto is unavailable), every primitive comes from
the audited noble suite: @noble/curves for P-256 ECDH and ES256,
@noble/hashes for HKDF/SHA-256, and @noble/ciphers for AES-128-GCM. To
deliver, it derives a fresh ephemeral P-256 key against the client's p256dh,
derives the content-encryption key and nonce (deriveWebPushEncryptionKeys),
AES-128-GCM encrypts and frames the payload, and signs a VAPID ES256 JWT
(buildVapidAuthorizationHeader). The encryption is verified byte-for-byte
against the RFC 8291 Appendix A known-answer test in
web-push-transport.test.ts. A non-2xx push-service response is reported as
{ ok: false }, never faked.
ingestPushDeliveryReceipt maps a provider receipt
(sent | delivered | failed | revoked) onto a metric event and a token status,
flipping a token to revoked so it stops receiving sends.
SMS — verified-identity flows only, with A2P 10DLC and a cost ledger#
SMS is restricted to verified-identity and safety flows. SmsUseCase is:
login-code, password-reset, billing-alert, crisis-hotline, verified-reminder
planSmsDelivery(request) enforces the rules and computes cost:
- It throws for a US recipient when
a2p10DlcRegisteredis false (A2P 10DLC compliance is mandatory for US delivery). - It computes
segmentCountat 160 chars/segment (smsSegments) and prices the message fromSMS_PRICING_MINOR_USD, a per-country minor-USD table (US1,CA1,GB4,IE5,DE8,FR8,ES5,IT7,AU5,NZ8,MX5,BR4,IN3,ZA3), with aSMS_PRICING_FALLBACK_MINOR_USDof 6 for unlisted countries. - It returns a
SmsCostLedgerEntry(channel: 'sms-twilio', country, use-case,amountMinor = price * segmentCount,currency: 'USD',segmentCount).
ingestSmsStatusWebhook maps the Twilio status
(queued | sent | delivered | failed | undelivered) to a metric event and
suppresses the recipient on failed/undelivered. The real transport,
sendSmsViaTwilio, POSTs x-www-form-urlencoded To/From/Body to
https://api.twilio.com/2010-04-01/Accounts/<sid>/Messages.json with HTTP Basic
auth and reads the returned sid. The planner and pricing are real; live
sending needs Twilio credentials (OSHUN_TWILIO_ACCOUNT_SID,
OSHUN_TWILIO_AUTH_TOKEN, OSHUN_TWILIO_FROM, optional
OSHUN_TWILIO_A2P_10DLC_COUNTRIES).
Discord — AAA-creator community surfaces#
The Discord adapter is deny-by-default and bound to specific communities.
DiscordServerBinding.community is the literal union
'yemaya-aaa-creator-community' | 'project-obsidian-production'.
assertDiscordEntitlement(binding) throws unless the binding is installed and
holds both REQUIRED_DISCORD_SCOPES (applications.commands, bot); an
unbound server is refused. renderDiscordAskCommand reuses the Telegram
grounded-answer renderer (renderGroundedTelegramAnswer) so Sophia grounding
discipline matches the Telegram bot's. The boundary entry for discord-bot
allows only the editorial-briefing content class — Discord is for
creator-community editorial surfaces, not transactional or crisis delivery, and
it is not exposed on the contemplative product. The real transport
sendDiscordViaBot POSTs to
https://discord.com/api/v10/channels/<id>/messages with a Bot <token>
authorization header.
Slack — institutional Metis delivery#
The Slack adapter is gated to Metis institutions. assertSlackEntitlement
requires the binding to be installed, the metisInstitutionId to start with
the literal prefix metis-institution:, and both REQUIRED_SLACK_SCOPES
(commands, chat:write) to be present — otherwise it throws. Workspace
binding happens through a per-workspace OAuth install.
renderSlackOshunCommand handles /oshun today | save | ask with the same
grounding discipline as the Telegram bot:
/oshun askrequires a real grounded answer andGroundingState, rendered throughrenderGroundedTelegramAnswer; it throws if grounding input is missing./oshun todayrenders the recipient's realtodayItems—"Nothing is scheduled for you today."when the list is empty, never a fabricated agenda./oshun savereturns a real confirmation only when asavedSummaryis present; otherwise it honestly states"Saving from Slack is not connected, so I will not claim it saved."
The boundary allows the slack-app channel the transactional,
grounded-answer, and editorial-briefing content classes. The real transport
sendSlackViaWebApi POSTs to https://slack.com/api/chat.postMessage and
treats Slack's HTTP-200-with-{ ok:false } logical errors as failures.
The channel boundary, audit envelope, and memory guard#
boundary.ts is the implementation behind the narrative "channel boundary." Its
ContentClass union is:
transactional, grounded-answer, ritual-reminder, editorial-briefing,
identity-verification, billing, crisis
CHANNEL_BOUNDARIES lists, per channel, the allowedContentClasses, residency,
retention, the human-readable consent requirement, whether provenance is
preserved, the disclosureMechanism
(body-copy | body-copy-and-link | payload-metadata), and the auditEvents
emitted. For example, sms-twilio allows
identity-verification, billing, crisis, ritual-reminder with
provenancePreserved: false, while email-ses allows
transactional, billing, editorial-briefing, grounded-answer, ritual-reminder
with body-copy-and-link disclosure and standard-365d retention.
Every send/receive emits a ChannelAuditEnvelope built by
buildChannelAuditEnvelope, with the fields the docs describe narratively:
{
"channelId": "email-ses",
"recipientId": "...",
"intentClass": "grounded-answer",
"persona": "...",
"policyHash": "...",
"provenanceBundleId": "...",
"residencyTag": "global",
"retentionClass": "standard-365d",
"disclosureVerificationResult": "passed"
}
decideMemoryIngestion is the Iris memory guard: a durable write
(durableWriteAllowed) requires both the rememberFromChatOptIn flag and a
channel in DURABLE_CHAT_INGESTION_CHANNELS (Telegram bot/Mini App, WhatsApp,
Discord, Slack — never push, SMS, email, or a Telegram channel). It returns the
channel's retentionClass, a list of channel-specific redactionRules (e.g.
strip-phone-numbers, strip-device-tokens, strip-subscription-endpoint for
Web Push), and the provenance tuple { channelId, externalMessageId }.
consentRevocationCascade, crisisCrossChannelSuppression, and
tenantResidencyReport complete the boundary contract; see
Channel Abstraction, Routing, and Boundaries
and
Privacy, Consent, Data Portability, and User Controls
for the cascade detail.
The credential→transport seam — fail loud, never fake#
provider-config-env.ts is the safety property that ties everything together:
buildMessageProviderConfigFromEnv(env) includes a channel in the live
MessageProviderConfig only when every credential it needs is present. A
channel with missing credentials is simply omitted, so
deliverDispatchedMessage reports missing-config for it rather than silently
faking a send. No secret is ever logged, defaulted, or invented. The env keys
per channel are:
| Channel | Required env vars |
|---|---|
| Email (SMTP) | OSHUN_MESSAGING_EMAIL_FROM + OSHUN_SMTP_HOST (+ optional _PORT/_SECURE/_STARTTLS/_USERNAME/_PASSWORD) |
| Email (SendGrid) | OSHUN_MESSAGING_EMAIL_FROM + OSHUN_SENDGRID_API_KEY |
| SMS | OSHUN_TWILIO_ACCOUNT_SID, OSHUN_TWILIO_AUTH_TOKEN, OSHUN_TWILIO_FROM |
| Push (FCM) | OSHUN_FCM_ACCESS_TOKEN, OSHUN_FCM_PROJECT_ID |
| Push (APNs) | OSHUN_APNS_TEAM_ID, OSHUN_APNS_KEY_ID, OSHUN_APNS_PRIVATE_KEY, OSHUN_APNS_BUNDLE_ID |
| Push (Web Push) | OSHUN_VAPID_PUBLIC_KEY, OSHUN_VAPID_PRIVATE_KEY, OSHUN_VAPID_SUBJECT |
OSHUN_WHATSAPP_ACCESS_TOKEN, OSHUN_WHATSAPP_PHONE_NUMBER_ID, OSHUN_WHATSAPP_TEMPLATE, OSHUN_WHATSAPP_LANGUAGE |
|
| Slack | OSHUN_SLACK_BOT_TOKEN |
| Discord | OSHUN_DISCORD_BOT_TOKEN |
configuredChannelsFromEnv reports which channels are live in a given
environment, and TransportFetch (plus createHttp2TransportFetch for APNs and
the BinaryTransportFetch for Web Push) is injectable so every transport is
unit-testable without network or credentials. This is also why the
deliverability monitoring the prose describes — bounce/complaint rates
(email), delivery/undelivered (SMS), revoked tokens (push), template status
(WhatsApp) — is fed by real webhook-ingestion functions, not synthetic numbers.
Cross-domain integration: V3 deep links and session reminders#
V3 programming wires into these channels through two modules absent from the narrative docs:
v3-deep-links.ts— canonical share links withV3_SHARE_LINK_CANONICAL_HOST = 'app.oshun.com', the custom schemeoshun://v3(V3_SHARE_LINK_CUSTOM_SCHEME = 'oshun',V3_SHARE_LINK_CUSTOM_HOST = 'v3', path prefix/v3), and the share-channel setV3_MESSAGING_SHARE_CHANNELS = ['telegram', 'whatsapp', 'push', 'email', 'sms']. Surfaces include Tara bookings, Arete continuations, tickets, follows, and Saraswati editions.v3-session-reminders.ts— binds the V3 calendar to the dispatcher, enforcing the launch rule that a reminder may only be delivered inside the 30-minute pre-session window (V3_SESSION_REMINDER_MAX_LEAD_TIME_MINUTES = 30). It maps each share channel to a default channel ID (telegram → telegram-bot,whatsapp → whatsapp-business,push → push-fcm,email → email-ses,sms → sms-twilio).
The customer message center#
The prose mentions a "customer-visible message center"; the real substrate is
@oshun/customer-message-center
(customer-message-center/src/message-center.ts). It defines
CUSTOMER_MESSAGE_CHANNELS = ['in-app', 'email', 'push', 'sms', 'voice'], ten
CUSTOMER_MESSAGE_CATEGORIES
(onboarding, milestone, reengagement, billing, privacy, support, incident, content-update, social, system),
four CUSTOMER_MESSAGE_PRIORITIES (low, normal, high, critical), and an
eight-stage receipt model
CUSTOMER_MESSAGE_RECEIPT_STAGES = ['queued', 'sent', 'delivered', 'opened', 'clicked', 'failed', 'bounced', 'suppressed'].
This is the inbox state model that gives customers a unified view across the
channels above and flows into the BFF
(apps/oshun/bff/src/routes/customer-message-center.ts).
Honest status — what is shipped vs. provider-gated#
This area is implemented to an unusually high degree, and nothing in it is a result-faking stub. The honest edges:
- The policy core, planners, payload builders, and boundary are real and tested. The dispatcher, the cost ledgers, the WhatsApp session-window logic, the SMS pricing/A2P gate, the email DKIM/SPF/DMARC gate, and the entitlement asserts for Slack/Discord all run as pure, deterministic, unit-tested code.
- The transports are real protocol clients — the SMTP client (RFC 5321) and the Web Push transport (RFC 8030/8291/8188/8292, verified against the RFC 8291 Appendix A KAT) are genuine, dependency-light implementations, not wrappers.
- WhatsApp and SMS transports are planners whose live provider HTTP calls
require real credentials. With no credentials configured, the seam reports
missing-config— the fail-loud, never-fake behavior. - Outbound Telegram delivery is the noted gap. Per the completeness audit,
the BFF end-to-end outbound Telegram send loop is
unimplemented; the policy core and Sophia grounding are real. The Telegram voice-STT path is now a genuine fail-closed seam, not a stub — any doc calling it a stub is stale. See Telegram Surfaces for that surface in depth.
The backlog for this subsystem is §26 (channels); the feature-hub section lives in ../features.md. Residency and consent obligations are tracked under deps§26.