Oshun Platform · Architecture

Messaging Channels

A focused page within the Oshun Platform Architecture documentation. The full map and every sibling page live in the Architecture hub.

13sections17 minread4tables

On this page

Messaging Channels is the V1 subsystem that takes an outbound intent — a ritual reminder, a grounded answer, a transactional receipt, a crisis hotline — and decides whether, on which channel, in which content variant, and under whose consent and residency it is allowed to reach a member, then hands a normalized result back to the caller. It serves every customer-facing domain that wants to leave the web app (Telegram chat and Mini Apps, WhatsApp, email, push, SMS, Slack, Discord) plus the in-app message center. It sits between the domain runtimes and the real provider APIs in the V1 architecture set hubbed at ../ARCHITECTURE.md. Product scope: V1/features.md § Messaging Channels and Conversational Surfaces.

Read this for shipping vs. spec. This area is implemented to an unusually high degree. The routing core, channel boundary, audit envelope, credential seam, and the email / Web Push / Telegram transports are real, in-repo, and tested — including a dependency-free RFC 5321 SMTP client and a Web Push transport verified byte-for-byte against the RFC 8291 Appendix A known-answer test. The honest aspirational edge: end-to-end outbound Telegram delivery in the BFF e2e is unimplemented (the policy core and grounding are real; the live send loop is the gap, per the completeness audit). The WhatsApp / SMS / payment paths are pure planners whose actual provider HTTP calls only fire when real credentials are present. Nothing in this area is a result-faking stub: where an integration is absent, the code reports missing-config or throws *_not_configured rather than fabricating a send.

Everything below is grounded in libs/oshun/messaging-channels/ (the channel library), apps/oshun/telegram-bot/ and apps/oshun/telegram-miniapp/ (the two Telegram apps), apps/oshun/bff/src/routes/telegram.ts (the webhook), and libs/oshun/customer-message-center/ (the in-app inbox).

Where this subsystem sits#

  • Purpose: a single tier-aware routing layer that picks the right outbound channel per consent, residency, retention, capability, quiet-hours, frequency-cap, crisis-state, and audience tier, then drives a real provider transport — or honestly suppresses / reports a gap.
  • Library: libs/oshun/messaging-channels, published as @oshun/messaging-channels. Its barrel src/index.ts re-exports the registry, dispatcher, boundary, transports, the SMTP and Web Push transports, the per-channel modules (telegram/, whatsapp/, push/, sms/, discord/, slack/, email/), the reminder producers/scheduler/worker, the V3 deep-link and session-reminder helpers, and provider-config-env.
  • Apps: @oshun/telegram-bot (a real grammY app) and apps/oshun/telegram-miniapp (a Next app rendering the seven Studio surfaces inside Telegram).
  • Inbox substrate: libs/oshun/customer-message-center — the customer-visible message center state model, plus its BFF wiring under apps/oshun/bff/src/customer/ and apps/oshun/bff/src/routes/customer-message-center.ts.

The channel registry — a per-channel policy matrix#

The prose summary of "channel abstraction and tier-aware routing" is backed by a concrete, frozen policy matrix in src/registry.ts. There are twelve channel ids, not the three or four the older feature docs name:

text
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 CHANNEL_REGISTRY entry is a ChannelEntry carrying the policy attributes the dispatcher consults, drawn from these enumerations:

Enumeration Values
CAPABILITY_KINDS text, rich-cards, inline-buttons, voice, files, payments, miniapp
COST_CLASSES free, low, metered, high
RESIDENCY_PROFILES global, eu-only, us-only, apac-only, self-hosted
CONSENT_PROFILES platform-tos-implied, verified-opt-in-required, double-opt-in-required
RETENTION_PROFILES channel-default, short-30d, standard-365d, long-3y, indefinite

A representative slice of the registry shows how policy differs per channel — this is the data that makes routing tier- and residency-aware rather than a generic fan-out:

Channel id Capabilities (subset) Cost Consent profile Retention Crisis-capable
telegram-bot text, rich-cards, inline-buttons, voice, files, payments, miniapp low verified-opt-in-required channel-default yes
telegram-channel text, rich-cards, files low verified-opt-in-required channel-default no
whatsapp-business text, rich-cards, inline-buttons, voice, files metered double-opt-in-required short-30d yes
sms-twilio text metered double-opt-in-required short-30d yes
push-fcm / -apns / -expo / -webpush text, rich-cards, inline-buttons free verified-opt-in-required short-30d no
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
email-ses text, rich-cards, files low verified-opt-in-required standard-365d no

Two registry helpers, lookupChannel(id) and listChannelsWith(capability), let the dispatcher and callers query the matrix. The crisisCapable flag is load-bearing: a crisis-hotline intent is only ever allowed to route to a channel that can safely carry hotline copy (telegram-bot, telegram-miniapp, whatsapp-business, sms-twilio).

Naming note on email-ses. The registry id is email-ses, but there is no dedicated SES API transport in the lib. The real email transports are sendEmailViaSendgrid (the SendGrid HTTPS API) and sendEmailViaSmtp (the hand-written RFC 5321 SMTP client, usable against AWS SES SMTP, Postmark, or the dev-stack Mailpit sink). The email-ses id is a naming artifact, and DKIM/SPF/DMARC are deploy-config concerns, not code in this library.

The dispatcher — tier-aware routing as a pure function#

src/dispatcher.ts exports dispatchMessage(input: DispatchInput): DispatchDecision — a pure function (pure-data in, pure-data out; the actual provider call is the runtime's job). It is the concrete enforcement of "tier-aware routing."

Tiers and the hard tier guard#

text
TIERS = ['contemplative', 'curated-creator', 'aaa-creator', 'operator-admin']

Critically, MESSAGING_ALLOWED_TIERS = new Set(['contemplative', 'curated-creator']). The very first check in dispatchMessage rejects any aaa-creator or operator-admin intent with { kind: 'suppress', reason: 'tier-not-allowed-on-messaging' }. No AAA or operator-admin intent can reach a member's channel — that capability is fenced off in one place, by design, so high-volume creator or operator tooling cannot accidentally spray messaging surfaces.

Intents and content classes#

text
INTENT_KINDS = [
  'transactional-receipt', 'crisis-hotline', 'reminder',
  'session-notification', 'newsletter', 'engagement-recap',
]

Each intent maps to a ContentClass via INTENT_CONTENT_CLASSES (contentClassForIntent), e.g. reminder → ritual-reminder, newsletter → editorial-briefing, engagement-recap → grounded-answer, crisis-hotline → crisis. The caller may override with an explicit contentClass.

Variants and capability fall-through#

A DispatchInput carries an ordered list of ContentVariants, each with a requiredCapability, bodyTextPlain, disclosureCopy, and a provenanceFooter. For a chosen channel, chooseVariantForChannel walks the variants and returns the first whose requiredCapability the channel supports; if none match but the channel supports text, it falls through to the explicit text variant (or coerces the first variant to text). This is how a rich-card answer gracefully degrades to a plain-text SMS without the caller having to branch.

The decision ladder#

dispatchMessage evaluates guards in this order, returning a typed suppress reason at the first failure (only crisis-hotline bypasses the suppression / quiet-hours / frequency guards, via CRISIS_SUPPRESSION_BYPASS):

  1. Tier guardtier-not-allowed-on-messaging
  2. Crisis-state suppressioncrisis-suppression
  3. Quiet hoursquiet-hours
  4. Frequency capfrequency-cap
  5. Empty tenant allowlisttenant-allowlist-empty
  6. Per-channel pass over preferredChannels: must be in tenantAllowlist, in userOptIns, satisfy residency (residencyMatches), be crisisCapable if the intent is crisis-hotline, and allow the content class (channelAllowsContentClass, consulting CHANNEL_BOUNDARIES). The first channel that also yields a feasible variant returns { kind: 'dispatch', channel, contentClass, chosenVariant }.
  7. If nothing dispatches, a diagnostic tail returns the most specific reason: no-opt-in, no-channel-meets-residency, content-class-not-allowed, or no-channel-meets-capability.

Tenant allowlist resolution#

resolveTenantChannelAllowlist(input: TenantChannelAllowlistInput) derives the tenant's permitted channel set from a residencyProfile plus disclosure-accept flags. The defaults encode real residency posture: eu-only['whatsapp-business', 'email-ses']; us-only['email-ses', 'sms-twilio']; self-hosted['email-ses']; otherwise the global default. Telegram and push channels are only added when the corresponding data-residency disclosure has been accepted (telegramDataResidencyDisclosureAccepted, pushDataResidencyDisclosureAccepted); Slack requires institutionalMessagingEnabled and Discord requires creatorCommunityMessagingEnabled. tenantPolicyAllowsChannel re-checks each of these so an explicit allowlist cannot smuggle a channel past residency policy.

The channel boundary and audit envelope#

src/boundary.ts is the real implementation of what the architecture overview describes narratively as "boundary, residency, retention, consent, audit." It declares a CHANNEL_BOUNDARIES table — one ChannelBoundary per channel id — plus the audit and memory-ingestion shapes.

ContentClass and per-channel allowances#

text
type ContentClass =
  | 'transactional' | 'grounded-answer' | 'ritual-reminder'
  | 'editorial-briefing' | 'identity-verification' | 'billing' | 'crisis'

Each ChannelBoundary lists which ContentClasses the channel may carry, its residency, retention, the human-readable consentRequired string, whether provenancePreserved is true, a disclosureMechanism (body-copy, body-copy-and-link, or payload-metadata), and the auditEvents it emits. For example, telegram-bot allows grounded-answer, ritual-reminder, identity-verification, and crisis (with audit events telegram.receive, telegram.send, telegram.crisis-suppression). sms-twilio allows identity-verification, billing, crisis, ritual-reminder and explicitly sets provenancePreserved: false (SMS cannot carry a provenance footer), so its disclosureMechanism is body-copy.

ChannelAuditEnvelope#

Every outbound send produces a full audit envelope via buildChannelAuditEnvelope. Its fields are:

Field Meaning
channelId which of the 12 channels
recipientId the bound recipient
intentClass the ContentClass actually delivered
persona the persona that authored the content
policyHash hash of the policy that authorized the send
provenanceBundleId link to the ProvenanceBundle
residencyTag the ResidencyProfile applied
retentionClass the RetentionProfile applied
disclosureVerificationResult 'passed' or 'failed'

Memory ingestion gating (the durable-write boundary)#

decideMemoryIngestion returns a MemoryIngestionDecision whose durableWriteAllowed is true only when the user opted into chat memory and the channel is in DURABLE_CHAT_INGESTION_CHANNELS (telegram-bot, telegram-miniapp, whatsapp-business, discord-bot, slack-app). Push and SMS payloads are excluded by construction — they can never write durable assistant memory. The decision also carries channel-specific redactionRules (e.g. strip-initdata, strip-miniapp-session-token for the Mini App; strip-subscription-endpoint for Web Push) and the inbound provenance. This is the seam where the messaging boundary defers to the Iris memory substrate — see Iris — Assistant Memory Substrate. Two more boundary helpers round out consent control: consentRevocationCascade (revoke binding, terminate Mini App sessions, cancel scheduled sends, redact chat cache, post a revocation notice) and crisisCrossChannelSuppression.

Real provider transports#

src/transports.ts is where the audit's earlier finding — "Email/SMS/Push/ WhatsApp were payload builders that never sent" — was closed. Each transport POSTs the correctly-shaped request to the provider's HTTP API and returns a normalized MessageTransportResult { ok, status, providerMessageId, error? }. The TransportFetch is injectable so the transport is unit-testable without network or credentials; credentials are deploy config (env), not code. On any non-ok response, failure(response) returns { ok: false } with the provider body — never a fabricated success.

Export Provider endpoint
sendEmailViaSendgrid https://api.sendgrid.com/v3/mail/send
sendSmsViaTwilio https://api.twilio.com/2010-04-01/Accounts/<sid>/Messages.json
sendPushViaFcm https://fcm.googleapis.com/v1/projects/<p>/messages:send
sendPushViaApns https://api.push.apple.com/3/device/<token> (ES256 provider JWT)
sendPushViaExpo https://exp.host/--/api/v2/push/send
sendWhatsAppViaMetaCloud https://graph.facebook.com/v21.0/<phoneNumberId>/messages
sendSlackViaWebApi https://slack.com/api/chat.postMessage
sendDiscordViaBot https://discord.com/api/v10/channels/<id>/messages

Two transports deserve a dedicated callout because they are full, RFC-grade protocol implementations rather than thin HTTP wrappers:

SMTP — src/smtp-email-transport.ts (RFC 5321)#

A real, dependency-free SMTP client over node:net / node:tls. It speaks the protocol directly — greeting → EHLO → optional STARTTLS → optional AUTHMAIL FROMRCPT TODATAQUIT — parses multiline replies, dot-stuffs the message body, and returns the same MessageTransportResult the HTTP transports return. It never fabricates success: any non-2xx reply, a dropped connection, or a reply timeout (default 15s) yields { ok: false } with the offending status/text. The socket factory (SmtpConnectionFactory) and the STARTTLS upgrade (SmtpTlsUpgrade) are injectable, so the entire protocol state machine is tested against an in-process SMTP server with real sockets. This is what lets the dev stack exercise the signup email-verification round-trip through Mailpit (the plain SMTP sink on :1025) end-to-end without a real provider.

Web Push — src/web-push-transport.ts (RFC 8030 / 8291 / 8188 / 8292)#

The W3C Web Push transport for PWAs. A PushSubscription { endpoint, keys: { p256dh, auth } } is not an FCM/APNs token, so delivery must: ECDH a fresh ephemeral P-256 key against the client's p256dh, derive the IKM (RFC 8291 §3.4) then the aes128gcm content-encryption key and nonce (RFC 8188), AES-128-GCM-encrypt and frame the payload, sign a VAPID ES256 JWT (RFC 8292), and POST the binary body. Because this library is client-transpiled (so node:crypto is unavailable), every primitive is the audited noble suite: @noble/curves (P-256 ECDH + ES256), @noble/hashes (HKDF/SHA-256), @noble/ciphers (AES-128-GCM), plus the Web Crypto CSPRNG for the salt. The encryption is verified byte-for-byte against the RFC 8291 Appendix A known-answer test in web-push-transport.test.ts. Exported building blocks include deriveWebPushEncryptionKeys, webPushEcdhSecret, encryptWebPushPayload, audienceForEndpoint, buildVapidAuthorizationHeader, and sendPushViaWebPush. A non-2xx push-service response is reported as { ok: false }, never a faked send.

APNs needs HTTP/2. Node's global fetch is HTTP/1.1; APNs requires HTTP/2. src/http2-transport-fetch.ts exports createHttp2TransportFetch for exactly this. provider-config-env.ts even surfaces a boot warning (apnsHttp2BootWarning) when APNs credentials are configured but no HTTP/2 transport is wired into the delivery path — a silent runtime gap turned into a loud startup string.

The credential seam — fail-loud, never faked#

src/provider-config-env.ts is the seam between deploy-time secrets and the transports. buildMessageProviderConfigFromEnv(env) reads OSHUN_* environment variables and includes a channel in the MessageProviderConfig only when every credential it needs is present. A channel with missing credentials is simply omitted — so deliverDispatchedMessage (in src/delivery.ts) returns { delivered: false, reason: 'missing-config', detail: '<channel>' } for it rather than silently faking a send. No secret is ever logged, defaulted, or invented.

Concretely: email requires OSHUN_MESSAGING_EMAIL_FROM plus a transport (OSHUN_SMTP_HOST takes precedence over OSHUN_SENDGRID_API_KEY); SMS requires OSHUN_TWILIO_ACCOUNT_SID + _AUTH_TOKEN + _FROM; FCM push requires OSHUN_FCM_ACCESS_TOKEN + _PROJECT_ID; APNs requires the four OSHUN_APNS_* values (team id, key id, .p8 private key PEM, bundle id); Web Push requires all three VAPID values; WhatsApp requires access token, phone number id, template, and language. configuredChannelsFromEnv reports exactly which channels are live. deliverWithEnvProviders composes this config with the delivery router so a reminder scheduler or notification worker can turn a dispatch decision plus a recipient into a real send, with credentials supplied purely by the environment. The full result space — missing-config / missing-recipient / suppressed / policy-blocked / missing-attribution / provider-error — is honest at every branch.

Telegram (bot, channel, Mini App)#

Telegram is the most fully built channel family, spanning three registry ids.

Mini App initData verification — src/telegram/security.ts#

Verification uses the noble HMAC/SHA-256 primitives directly. The two verifiers differ exactly as Telegram's spec requires:

  • verifyTelegramMiniAppInitData derives the secret as hmacSha256('WebAppData', botToken), then compares the supplied hash against hmacSha256Hex(secret, dataCheckString) in constant time.
  • verifyTelegramLoginWidgetPayload instead uses secret = sha256(botToken).

Both build the dataCheckString by sorting all params except hash and joining key=value\n, reject stale or future-skewed auth_date and replayed query_id/id, and return a typed reason (missing-hash / missing-subject / expired / tampered / replay). issueTelegramMiniAppSession then mints an HS256 JWT scoped to the allowed Telegram scopes (content:read, notebook:save, ritual:play, sky:view, qa:ask), and requiresPrimaryCredentialChallenge flags the sensitive actions (e.g. admin:access, consent:change, billing:manage, generation:promote-aaa) that demand a step-up.

Bot commands and grounding — src/telegram/bot.ts#

TelegramUpdateKind covers message, callback_query, inline_query, edited_message, and my_chat_member. All nine documented commands are implemented: /start, /menu, /today, /save, /sources, /voice, /quiet, /stop, /help. The handler is candid about what is not yet wired: the side-effecting commands (/save, /quiet, /stop, /voice) return honest "not connected yet" copy (e.g. "Saving from Telegram is not connected yet, so I will not pretend it saved.") rather than claiming a side effect. Every reply builds a ChannelAuditEnvelope. When the Sophia grounder cannot ground an answer (no sources / retrieval unavailable), the bot emits TELEGRAM_ABSTENTION_COPY"I can't ground that in sources right now, so I won't guess…" — instead of guessing. Voice messages route through a VoiceProvider from src/telegram/stt-provider.ts, which is a fail-closed seam: createSttVoiceProvider performs the real Telegram getFile → download → STT POST chain and unconfiguredVoiceProvider throws stt_not_configured — it never fabricates a transcript. (Any doc still calling the Telegram voice-STT path a "stub" is stale.)

The grammY app — apps/oshun/telegram-bot#

@oshun/telegram-bot is a real grammY application (package.json declares grammy ^1.42.0; main is src/index.ts). TELEGRAM_BOT_ENVIRONMENTS defines dev / staging / prod with env-var tokens and secrets, and the UNCONFIGURED_PROD_BOT_TOKEN = 'prod-token' sentinel makes createOshunGrammyBot fail fast in prod if the real token is unset — a misconfigured prod bot refuses to boot rather than run on a placeholder.

The Mini App surfaces — apps/oshun/telegram-miniapp#

The Next app renders all seven Studio surfaces inside Telegram, defined in src/app/surface-data.ts as SurfaceSlugs: today (Tara ritual), sophia (grounded Q&A), veritas (a veritas-claim with sources, counterclaims, confidence, save, and share), nyx (sky), arete (check-in), nisaba (reader), and illustration (an illustration-card). src/lib/telegram-webapp.ts bridges the Telegram.WebApp host. These surfaces are the "curated Studio surfaces in chat" the architecture overview refers to.

The BFF webhook#

apps/oshun/bff/src/routes/telegram.ts registers POST /telegram/webhook, guarded by requireWebhookSecret() against the x-telegram-bot-api-secret-token header (a bad secret returns 401 invalid-webhook-secret). The webhook handler in apps/oshun/bff/src/telegram/webhook.ts wires grounding via createTelegramSophiaGrounder({ retriever, minimumSources: 1 }) — the policy core and grounding are real. The honest gap: end-to-end outbound Telegram delivery in the BFF e2e is noted unimplemented in the completeness audit; the live send loop is the piece that is not yet exercised end-to-end.

WhatsApp, SMS, Slack, Discord#

These four channels ship real, pure-data planners with entitlement and cost/consent logic; the actual outbound HTTP fires through the transports above only when credentials are present.

WhatsApp — src/whatsapp/index.ts#

WhatsAppUseCasereceipt, reminder, password-reset, login-code, billing-alert, scheduled-event-nudge. planWhatsAppTemplateDelivery enforces the Meta 24-hour customer-initiated session window (SESSION_WINDOW_SECONDS = 24 * 60 * 60) and falls back to email when no approved template exists, recording a CostLedgerEntry per message. ingestWhatsAppStatusWebhook maps provider statuses (sent/delivered/read/failed/opted_out).

SMS — src/sms/index.ts#

SmsUseCaselogin-code, password-reset, billing-alert, crisis-hotline, verified-reminder. planSmsDelivery throws if a US send lacks A2P 10DLC registration, computes segment count, and prices the send from SMS_PRICING_MINOR_USD (a per-country table in USD minor units: US/CA 1, BR 4, GB 4, IE/ES/AU/MX 5, IT 7, DE/FR/NZ 8, IN/ZA 3, with a 6 fallback), emitting an SmsCostLedgerEntry.

Slack — src/slack/index.ts#

assertSlackEntitlement requires an installed binding whose metisInstitutionId starts with metis-institution: and that holds REQUIRED_SLACK_SCOPES = ['commands', 'chat:write']. renderSlackOshunCommand handles /oshun today|save|ask with real todayItems — no fabricated list. This is the institutional surface the architecture overview mentions ("V1 includes Slack notification sinks for tenants").

Discord — src/discord/index.ts#

DiscordServerBinding.community is constrained to 'yemaya-aaa-creator-community' or 'project-obsidian-production', REQUIRED_DISCORD_SCOPES = ['applications.commands', 'bot'], and assertDiscordEntitlement is deny-by-default for an unbound server.

Push surface (broader than FCM + APNs)#

The push surface is wider than the older feature docs state. The registry has four push channel ids — push-fcm, push-apns, push-expo, push-webpush — backed by sendPushViaFcm, sendPushViaApns, sendPushViaExpo, and sendPushViaWebPush. The payload builder in src/push/index.ts (PushPlatform = 'fcm-android' | 'fcm-web' | 'apns-ios', buildPushPayload) shapes the platform-specific bodies, while Expo (React Native clients registering an ExponentPushToken[...]) and Web Push (PWAs with a VAPID subscription) are first-class additional delivery paths, not afterthoughts.

V3 cross-domain integration#

Two helpers wire V3 programming into messaging.

  • Deep links — src/v3-deep-links.ts. V3_SHARE_LINK_CANONICAL_HOST = 'app.oshun.com' (origin https://app.oshun.com), with a custom scheme oshun://v3 (V3_SHARE_LINK_CUSTOM_SCHEME = 'oshun', V3_SHARE_LINK_CUSTOM_HOST = 'v3') and path prefix /v3. Shares route over V3_MESSAGING_SHARE_CHANNELS = ['telegram', 'whatsapp', 'push', 'email', 'sms']. buildV3MessagingSharePayload produces both the canonical HTTPS URL and the custom-scheme URL.
  • Session reminders — src/v3-session-reminders.ts. A pre-session reminder may only be delivered inside the 30-minute pre-session window (V3_SESSION_REMINDER_MAX_LEAD_TIME_MINUTES = 30); outside it, planV3SessionReminderDelivery suppresses with outside-thirty-minute-window. Push provider selection (fcm/apns/expo/webpush) resolves to the right ChannelId.

The in-app message center#

libs/oshun/customer-message-center is the customer-visible inbox substrate the architecture overview gestures at. src/message-center.ts defines the real state model:

  • CUSTOMER_MESSAGE_CHANNELS = ['in-app', 'email', 'push', 'sms', 'voice']
  • CUSTOMER_MESSAGE_CATEGORIES (10): onboarding, milestone, reengagement, billing, privacy, support, incident, content-update, social, system
  • CUSTOMER_MESSAGE_RECEIPT_STAGES = ['queued', 'sent', 'delivered', 'opened', 'clicked', 'failed', 'bounced', 'suppressed']
  • CUSTOMER_MESSAGE_PRIORITIES = ['low', 'normal', 'high', 'critical']

The BFF surfaces it through apps/oshun/bff/src/routes/customer-message-center.ts and apps/oshun/bff/src/customer/customer-message-center-store.ts.

Honest status — what is real, what is gated#

  • Real and tested: the registry policy matrix, the pure-function dispatcher (tier guard, intent→content-class, capability fall-through, residency and consent gates), the channel boundary + audit envelope + memory-ingestion gate, the credential→transport seam with missing-config fail-loud, the SendGrid/Twilio/FCM/APNs/Expo/Meta/Slack/Discord transports, the dependency-free SMTP client, and the Web Push transport (RFC 8291 Appendix A KAT). Telegram initData verification, all 9 bot commands, the abstention copy, the fail-closed voice-STT seam, all 7 Mini App surfaces, and the webhook-secret guard are in-repo.
  • Planner-level, credential-gated: WhatsApp, SMS, and the payment paths are pure planners whose real provider HTTP calls only fire when credentials are set — by design, they report a gap rather than fake a send.
  • Aspirational edge: end-to-end outbound Telegram delivery in the BFF e2e is unimplemented (policy core and grounding are real; the live send loop is the gap). DKIM/SPF/DMARC remain deploy-config concerns, and the email-ses id has no dedicated SES API transport (SendGrid + SMTP cover email).

Cross-cutting controls referenced here — consent, residency, retention, provenance, and audit — connect to Security, Privacy, and Compliance, Trust, Safety, and Privacy, and the grounding substrate Sophia — Grounding Substrate. Backlog items for this area live in V1/TODOS.md; cross-domain dependencies in V1/DEPENDENCIES.md.