This page documents the channel-abstraction layer that lets V1 deliver content
over many messaging surfaces — Telegram, WhatsApp, email, push, SMS, Discord,
Slack — through one common registry, one routing dispatcher, and one boundary
contract, instead of bespoke glue per channel. It serves customers (delivery to
their chosen channel) and tenants/operators (residency-aware allowlists and a
full audit trail). It sits beneath the per-surface pages
Telegram Surfaces and
WhatsApp, Email, Push, SMS, Discord, and Slack, which
describe each transport in depth. The single rule that shapes everything here:
no AAA-creator or operator-admin intent ever reaches a messaging channel, and
nothing is ever reported as "sent" unless a real provider transport actually
sent it. Almost all of this is implemented in
libs/oshun/messaging-channels/src; the honest gaps are named explicitly at the
end.
Where this lives in V1#
The whole subsystem is one Nx library, @oshun/messaging-channels
(libs/oshun/messaging-channels/src), whose index.ts re-exports the registry,
dispatcher, boundary, transports, provider-config seam, per-channel adapters,
the Telegram bot/security stack, reminder workers, the V3 deep-link/session
modules, and the channel-binding store. Three files form the spine:
registry.ts— the per-channel capability and policy matrix (what a channel can do, its cost, residency, consent, and retention profile).dispatcher.ts— the pure routing function that turns an(audience, intent, content variants, persona, residency)tuple into aDispatchDecisionofdispatch(to a concrete channel) orsuppress(with a typed reason).boundary.ts— the content-class allowlist, audit envelope, and memory ingestion guard that govern what may cross a channel and how it is recorded.
provider-config-env.ts is the seam from those pure-data decisions to the real
provider transports in transports.ts, smtp-email-transport.ts, and
web-push-transport.ts. The companion engineering reference is
../architecture/messaging-channels.md.
The channel registry — a per-channel policy matrix#
registry.ts enumerates twelve concrete channels in CHANNEL_IDS, not the
narrower "Telegram + WhatsApp + email + push (FCM/APNs) + SMS + Discord + Slack"
list the prose summary suggests. The full set is:
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 maps to a frozen ChannelEntry in CHANNEL_REGISTRY carrying a
ReadonlySet<CapabilityKind> plus a cost, residency, consent, retention,
disclosure-renderable flag, and a crisisCapable flag. The five enumerations
the dispatcher reads are:
| 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 |
The matrix is what makes routing data-driven rather than hand-coded. A few
representative entries from CHANNEL_REGISTRY:
| Channel | Capabilities | Cost | Consent | Retention | Crisis-capable |
|---|---|---|---|---|---|
telegram-bot |
text, rich-cards, inline-buttons, voice, files, payments, miniapp | low |
verified-opt-in-required |
channel-default |
yes |
whatsapp-business |
text, rich-cards, inline-buttons, voice, files | metered |
double-opt-in-required |
short-30d |
yes |
sms-twilio |
text only | metered |
double-opt-in-required |
short-30d |
yes |
discord-bot |
text, rich-cards, inline-buttons, voice, files | free |
platform-tos-implied |
channel-default |
no |
email-ses |
text, rich-cards, files | low |
verified-opt-in-required |
standard-365d |
no |
push-* (fcm/apns/expo/webpush) |
text, rich-cards, inline-buttons | free |
verified-opt-in-required |
short-30d |
no |
Two helpers query the matrix: lookupChannel(id) returns an entry, and
listChannelsWith(capability) returns every channel id that supports a given
capability — the dispatcher uses the latter family of facts to fall through from
a rich variant to a plain-text one when a channel can't render it.
Why crisisCapable is a first-class field#
Crisis content (verified hotlines, plain operator voice) must never be routed to
a surface that can't carry it safely. The registry marks telegram-bot,
telegram-miniapp, whatsapp-business, and sms-twilio as
crisisCapable: true and everything else false. The dispatcher refuses to
send a crisis-hotline intent to a non-crisis-capable channel even if it is
otherwise allowed — see the routing walkthrough below.
The dispatcher — tier-aware routing as a pure function#
dispatchMessage(input: DispatchInput): DispatchDecision in dispatcher.ts is
a pure function: pure-data in, pure-data out, no I/O. The actual provider
call is the runtime's job, downstream of the decision. This separation is what
makes the routing policy exhaustively unit-testable.
Audience tiers and the messaging guard#
TIERS = ['contemplative', 'curated-creator', 'aaa-creator', 'operator-admin']
The internal constant MESSAGING_ALLOWED_TIERS is
Set(['contemplative', 'curated-creator']). The very first check in
dispatchMessage is the tier guard: any input whose tier is aaa-creator or
operator-admin returns
{ kind: 'suppress', reason: 'tier-not-allowed-on-messaging' } before any
channel is even considered. This is the concrete, code-level enforcement of the
audience-tier discipline described narratively in the product promise — AAA
production coordination and operator/admin surfaces simply have no path to a
messaging channel. See
Generation Audience Tiers and Surface Boundaries
for the same tier vocabulary on the generation side, and §26.1 in the backlog.
Intent kinds and content variants#
The dispatcher reasons over six intent kinds, each statically mapped to a content class:
IntentKind |
Mapped ContentClass |
|---|---|
transactional-receipt |
transactional |
crisis-hotline |
crisis |
reminder |
ritual-reminder |
session-notification |
transactional |
newsletter |
editorial-briefing |
engagement-recap |
grounded-answer |
contentClassForIntent(intentKind) exposes that mapping; an explicit
contentClass on the input overrides it. The caller supplies an ordered list of
ContentVariants, each declaring a requiredCapability, the plain body text, a
disclosureCopy string, and a provenanceFooter. chooseVariantForChannel
walks the variants in order and picks the first whose requiredCapability the
channel supports; if none match, it falls back to a text variant (or
text-coerces the first variant) — but only when the channel supports text at
all. This is the "per-channel capability fall-through" the prose names: a
rich-card newsletter degrades to a text briefing on SMS rather than being
dropped, while preserving its disclosure and provenance footer.
The routing order — and every reason it can suppress#
dispatchMessage evaluates guards in a deliberate order, each producing a typed
suppress reason so the runtime can audit why nothing went out:
- Tier guard →
tier-not-allowed-on-messaging. - Crisis-state suppression. If
crisisSuppressionActiveand the intent is not inCRISIS_SUPPRESSION_BYPASS(which contains onlycrisis-hotline) →crisis-suppression. A crisis flag on the user therefore silences non-essential traffic but never blocks the hotline itself. - Quiet hours →
quiet-hours(samecrisis-hotlinebypass). - Frequency cap →
frequency-cap(same bypass). - Empty tenant allowlist →
tenant-allowlist-empty. - Preferred-channel loop. For each channel in
preferredChannels, in order, the channel must be in thetenantAllowlist, in the user'suserOptIns, satisfy the residency requirement (residencyMatches—globalmatches anything, otherwise an exact match), becrisisCapableif the intent iscrisis-hotline, and allow the content class per the boundary table. The first channel that clears all of these and has a feasible variant yields{ kind: 'dispatch', channel, contentClass, chosenVariant }. - Diagnosis. If no channel dispatched, the function works out the precise
reason:
no-opt-in,no-channel-meets-residency,content-class-not-allowed, or finallyno-channel-meets-capability.
The result is that "route by user preference, entitlement, residency, opt-in, quiet hours, and per-channel capability fall-through" is not aspirational prose — it is the literal branch structure of one tested function.
Tenant allowlists with residency-driven defaults#
resolveTenantChannelAllowlist(input: TenantChannelAllowlistInput) computes the
per-tenant allowlist. If the tenant supplies an explicitAllowlist, it is
filtered through tenantPolicyAllowsChannel; otherwise residency drives the
default set:
eu-only→['whatsapp-business', 'email-ses']us-only→['email-ses', 'sms-twilio']self-hosted→['email-ses']- everything else (global) →
['telegram-bot', 'telegram-miniapp', 'whatsapp-business', 'sms-twilio', 'email-ses']
Telegram and the four push channels are then added only behind explicit
data-residency disclosures for non-global tenants:
telegramDataResidencyDisclosureAccepted re-admits the three Telegram channels,
and pushDataResidencyDisclosureAccepted (or a us-only/global profile)
re-admits push-fcm/push-apns. Slack is gated on
institutionalMessagingEnabled, Discord on creatorCommunityMessagingEnabled.
This is exactly the "EU tenant favors WhatsApp + email; Telegram opt-in with
data-residency disclosure" behavior the prose describes — encoded, not assumed.
The channel boundary — what may cross, and how it is recorded#
boundary.ts defines the per-channel contract: which content classes are
permitted, the residency and retention profile, the consent language, whether
provenance survives, the disclosure mechanism, and the audit-event names. The
ContentClass union is:
type ContentClass =
| 'transactional' | 'grounded-answer' | 'ritual-reminder'
| 'editorial-briefing' | 'identity-verification' | 'billing' | 'crisis'
CHANNEL_BOUNDARIES lists one ChannelBoundary per channel. The
allowedContentClasses field is the gate the dispatcher consults via
channelAllowsContentClass. A few load-bearing rows:
| Channel | Allowed content classes | Disclosure mechanism | Provenance preserved |
|---|---|---|---|
telegram-bot |
grounded-answer, ritual-reminder, identity-verification, crisis | body-copy-and-link |
yes |
whatsapp-business |
transactional, identity-verification, billing, ritual-reminder | body-copy |
yes |
sms-twilio |
identity-verification, billing, crisis, ritual-reminder | body-copy |
no |
discord-bot |
editorial-briefing | body-copy-and-link |
yes |
email-ses |
transactional, billing, editorial-briefing, grounded-answer, ritual-reminder | body-copy-and-link |
yes |
push-* |
transactional, ritual-reminder, identity-verification | payload-metadata |
no |
Note the deliberate asymmetries: SMS and push do not preserve provenance
(there is nowhere to render a citation footer), so they are kept to terse
content classes. Discord is restricted to editorial-briefing only, reflecting
its community-broadcast role. WhatsApp carries no grounded-answer because its
template-only model can't host a live grounded Q&A.
The audit envelope#
Every send and receive carries a full ChannelAuditEnvelope, built by
buildChannelAuditEnvelope. Its shape is the complete record the prose calls
for:
interface ChannelAuditEnvelope {
channelId: ChannelId;
recipientId: string;
intentClass: ContentClass;
persona: string;
policyHash: string;
provenanceBundleId: string;
residencyTag: ResidencyProfile;
retentionClass: RetentionProfile;
disclosureVerificationResult: 'passed' | 'failed';
}
The Telegram bot (telegram/bot.ts) constructs one of these on every
BotResponse, so an operator can later reconstruct which persona spoke, under
which policy hash, with which provenance bundle, and whether the disclosure copy
verified — for any message the platform ever sent over chat.
The Iris memory-ingestion guard#
decideMemoryIngestion(...) is the gate that decides whether chat content may
become a durable Iris memory. It returns a MemoryIngestionDecision whose
durableWriteAllowed is true only when both the user has opted into
"remember from chat" (rememberFromChatOptIn) and the channel is in
DURABLE_CHAT_INGESTION_CHANNELS (telegram-bot, telegram-miniapp,
whatsapp-business, discord-bot, slack-app). Push and SMS payloads can
never seed durable memory. The decision also carries the channel's
retentionClass and a list of redactionRules — a base set
(strip-channel-handles, strip-phone-numbers, strip-payment-tokens) plus
channel-specific rules such as strip-initdata / strip-miniapp-session-token
for the Mini App, strip-subscription-endpoint for Web Push, and
strip-one-time-codes for SMS. This is the concrete implementation of "user
must explicitly opt in before any durable Iris write." See
Iris Memory and Identity and
Privacy, Consent, Data Portability, and User Controls.
Consent revocation and crisis cascades#
consentRevocationCascade(...)returns the ordered list of actions a binding revocation triggers:revoke-binding,terminate-miniapp-session:*for each active session,cancel-scheduled-send:*for each scheduled send,redact-chat-cache, andpost-revocation-notice. Pulling a binding therefore tears down subscriptions, sessions, and scheduled sends and redacts cached state in one pass.crisisCrossChannelSuppression(...)returns the deduplicated set of channels to suppress for a user once a crisis flag is raised on any one channel — feeding the dispatcher'scrisisSuppressionActiveguard so a crisis on Telegram silences non-essential sends on email and push too.tenantResidencyReport(...)renders a per-tenant, per-channel residency / content-class / retention summary string for compliance review.
The credential seam — fail-loud, never faked#
provider-config-env.ts is the bridge between deploy-time secrets and the real
transports, and it embodies the single most important safety property in this
area: a channel is included in the live MessageProviderConfig only when
every credential it needs is present. buildMessageProviderConfigFromEnv(env)
reads each channel's env vars and adds the channel only when all required
values are non-empty:
- Email —
OSHUN_MESSAGING_EMAIL_FROMplus a transport:OSHUN_SMTP_HOST(preferred when set, also used for the Mailpit dev sink, AWS SES SMTP, or Postmark SMTP) orOSHUN_SENDGRID_API_KEY. - SMS —
OSHUN_TWILIO_ACCOUNT_SID,OSHUN_TWILIO_AUTH_TOKEN,OSHUN_TWILIO_FROM(optionalOSHUN_TWILIO_A2P_10DLC_COUNTRIES). - Push (FCM) —
OSHUN_FCM_ACCESS_TOKEN,OSHUN_FCM_PROJECT_ID. - APNs —
OSHUN_APNS_TEAM_ID,OSHUN_APNS_KEY_ID,OSHUN_APNS_PRIVATE_KEY,OSHUN_APNS_BUNDLE_ID. - Expo — optional
OSHUN_EXPO_ACCESS_TOKEN. - Web Push — all three of
OSHUN_VAPID_PUBLIC_KEY,OSHUN_VAPID_PRIVATE_KEY,OSHUN_VAPID_SUBJECT. - WhatsApp —
OSHUN_WHATSAPP_ACCESS_TOKEN,OSHUN_WHATSAPP_PHONE_NUMBER_ID,OSHUN_WHATSAPP_TEMPLATE,OSHUN_WHATSAPP_LANGUAGE. - Slack / Discord —
OSHUN_SLACK_BOT_TOKEN/OSHUN_DISCORD_BOT_TOKEN.
When credentials are missing, the channel is simply omitted from the config, so
deliverDispatchedMessage reports missing-config for it — an explicit
fail-loud refusal, never a fabricated send. The module's own header states the
contract: "No secret is ever logged, defaulted, or invented."
deliverWithEnvProviders(...) composes this env config with the delivery router
so a reminder scheduler or notification worker can take a DispatchDecision +
recipient and actually send, with fetchImpl injectable for tests.
configuredChannelsFromEnv(env) reports which channels are live in a given
environment, and apnsHttp2BootWarning(...) returns a startup warning when APNs
is configured but no HTTP/2 transport is wired (Node's global fetch is
HTTP/1.1; APNs needs H2) — catching a silent runtime gap before it can drop
sends.
The real transports behind the seam#
Once a channel is configured, the decision flows to a real provider transport.
These are not plan-builders — each speaks the provider's actual protocol and
returns { ok: false } on any non-success. TransportFetch is injectable
throughout, so the transports are unit-tested without network or credentials.
transports.tsexportssendEmailViaSendgrid(SendGrid v3api.sendgrid.com/v3/mail/send),sendSmsViaTwilio(api.twilio.com/2010-04-01/Accounts/.../Messages.json),sendWhatsAppViaMetaCloud(graph.facebook.com/v21.0/.../messages),sendPushViaFcm(fcm.googleapis.com/v1/...),sendPushViaApns,sendPushViaExpo(exp.host/--/api/v2/push/send, withbuildApnsProviderJwt), plussendSlackViaWebApiandsendDiscordViaBot.smtp-email-transport.tsis a dependency-free RFC 5321 SMTP client overnode:net/node:tls. It speaks the protocol directly — greeting →EHLO→ optionalSTARTTLS→ optionalAUTH→MAIL FROM→RCPT TO→DATA→QUIT— reads multiline replies, dot-stuffs the body, and returns{ ok: false }on any non-2xx reply, dropped connection, or timeout. The socket factory (SmtpConnectionFactory) and STARTTLS upgrade (SmtpTlsUpgrade) are injectable, so the full state machine is tested against a real in-process SMTP server.web-push-transport.tsimplements the W3C Web Push Protocol (RFC 8030) withaes128gcmpayload encryption (RFC 8291 + RFC 8188) and VAPID application-server identification (RFC 8292). Because the lib is client-transpiled (node:cryptois noop'd by webpack), every primitive is the audited noble suite:@noble/curves(P-256 ECDH + ES256),@noble/hashes(HKDF/SHA-256),@noble/ciphers(AES-128-GCM). The encryption is verified byte-for-byte against the RFC 8291 Appendix A known-answer test inweb-push-transport.test.ts.http2-transport-fetch.tsexportscreateHttp2TransportFetch— the HTTP/2 transport APNs requires, also tested against an in-process h2c server.
Telegram security and the bot command surface#
The Telegram surface carries the strongest auth code in the area.
telegram/security.ts uses real HMAC with @noble/hashes:
verifyTelegramMiniAppInitDataderivessecret = hmacSha256('WebAppData', botToken), then compares the suppliedhashagainsthmacSha256Hex(secret, dataCheckString)in constant time — the exact key derivation Telegram specifies for Mini AppinitData.verifyTelegramLoginWidgetPayloadusessecret = sha256(botToken)instead, per the Login Widget spec.- Both reject stale (
auth_dateoutsidemaxAgeSeconds/skew) and replayed (query_id/idinconsumedIds) payloads, andissueTelegramMiniAppSessionmints a scoped HS256 JWT (iss: 'oshun-telegram',aud: 'oshun-telegram-miniapp') over the allowed scopescontent:read,notebook:save,ritual:play,sky:view,qa:ask.
telegram/bot.ts handles TelegramUpdateKind ∈ {message, callback_query,
inline_query, edited_message, my_chat_member} and implements all nine
documented commands. They split honestly into two groups:
- Read-only commands answered directly by
handleTelegramCommand:/start,/menu,/today,/sources,/help. - Side-effect commands that route through the effects port:
/save,/quiet,/stop,/voice(and/unlink). When no effects port is wired, each returns honest copy fromSIDE_EFFECT_NOT_CONNECTED— e.g. "Saving from Telegram is not connected yet, so I will not pretend it saved." — rather than faking the action.
When Sophia cannot ground an answer, the bot emits TELEGRAM_ABSTENTION_COPY
("I can't ground that in sources right now, so I won't guess.") instead of
guessing — the persona/grounding discipline of
Sophia Grounding and
Lilith Persona Policy carried onto chat.
The deployed bot lives in apps/oshun/telegram-bot as a real grammY app
(package.json dep grammy ^1.42.0, main = src/index.ts).
TELEGRAM_BOT_ENVIRONMENTS configures dev/staging/prod instances with env-var
tokens and secrets, and the UNCONFIGURED_PROD_BOT_TOKEN = 'prod-token'
sentinel makes createOshunGrammyBot fail fast in prod rather than run with a
placeholder token. On the BFF, apps/oshun/bff/src/routes/telegram.ts guards
/telegram/webhook with requireWebhookSecret() (checking the
x-telegram-bot-api-secret-token header), and
apps/oshun/bff/src/telegram/webhook.ts wires grounding with
createTelegramSophiaGrounder({ retriever, minimumSources: 1 }), falling back
to a fixture grounder only when no retriever is present.
The seven Telegram Mini App surfaces#
apps/oshun/telegram-miniapp is a Next app whose src/app/surface-data.ts
defines seven surface kinds — today (Tara), sophia, veritas, nyx,
arete, nisaba, and illustration. The veritas surface carries a
veritas-claim with counterclaims, and illustration carries an
illustration-card, mirroring the domains they front. telegram-webapp.ts
bridges the Telegram.WebApp runtime. These are detailed in
Telegram Surfaces.
The per-channel adapters (planners and their gates)#
Each thin-channel adapter encodes the policy specific to its provider, even where the live HTTP call is provider-gated:
- WhatsApp (
whatsapp/index.ts) —WhatsAppUseCase∈ {receipt,reminder,password-reset,login-code,billing-alert,scheduled-event-nudge}.planWhatsAppTemplateDeliveryenforces the Meta 24-hour customer-initiated session window (SESSION_WINDOW_SECONDS = 24*60*60) and falls back to a template message or email when no approved template applies; it emits aCostLedgerEntryper message. - SMS (
sms/index.ts) —SmsUseCase∈ {login-code,password-reset,billing-alert,crisis-hotline,verified-reminder} (verified-identity flows only).SMS_PRICING_MINOR_USDis a real per-country table (US/CA 1, GB 4, IE 5, DE/FR 8, …, fallback 6) and US delivery requires A2P 10DLC registration or the planner throws. - Slack (
slack/index.ts) —assertSlackEntitlementrequires the workspace to be installed, themetisInstitutionIdto start withmetis-institution:, and allREQUIRED_SLACK_SCOPES = ['commands', 'chat:write']to be present.renderSlackOshunCommandhandles/oshun today|save|askover realtodayItems(no fabricated list). - Discord (
discord/index.ts) —DiscordServerBinding.community∈ {yemaya-aaa-creator-community,project-obsidian-production},REQUIRED_DISCORD_SCOPES = ['applications.commands', 'bot'], deny-by-default for unbound servers. - Push (
push/index.ts) —PushPlatformenum is'fcm-android' | 'fcm-web' | 'apns-ios'for payload synthesis, whileCHANNEL_IDSadditionally carriespush-expoandpush-webpushfor their own transports.
All of these are detailed in WhatsApp, Email, Push, SMS, Discord, and Slack.
V3 cross-domain integration#
Two modules wire V3 programming into messaging:
v3-deep-links.tsdefinesV3_SHARE_LINK_CANONICAL_HOST = 'app.oshun.com', the custom schemeoshunwith hostv3(i.e.oshun://v3...), path prefix/v3, andV3_MESSAGING_SHARE_CHANNELS = ['telegram', 'whatsapp', 'push', 'email', 'sms']. It produces both canonical HTTPS and custom-scheme share links for surfaces like Tara bookings, Arete continuations, tickets, follows, and the Saraswati wallet/edition.v3-session-reminders.tsenforces a 30-minute pre-session reminder window (V3_SESSION_REMINDER_MAX_LEAD_TIME_MINUTES = 30); a reminder scheduled earlier than that returnsoutside-thirty-minute-window.
The customer message center substrate#
libs/oshun/customer-message-center/src/message-center.ts is the
customer-visible inbox state model that the dispatcher feeds.
CUSTOMER_MESSAGE_CHANNELS = ['in-app', 'email', 'push', 'sms', 'voice'];
CUSTOMER_MESSAGE_CATEGORIES has ten values (onboarding, milestone,
reengagement, billing, privacy, support, incident, content-update,
social, system); and
CUSTOMER_MESSAGE_RECEIPT_STAGES = ['queued', 'sent', 'delivered', 'opened', 'clicked', 'failed', 'bounced', 'suppressed']
is the delivery-receipt lifecycle each message moves through. Deliverability
metrics (bounce, open, click, push-grant, SMS-failure) are derived from this
delivery-status lifecycle in message-center.ts.
What is real, and what is honestly still planned#
This area is implemented to an unusually high degree, and nothing in it is a result-faking stub:
- The tier guard, the registry/boundary matrices, the dispatcher's full
suppress-reason taxonomy, the SMTP and Web Push RFC-grade transports, the
Telegram HMAC verification, all nine bot commands, all seven Mini App
surfaces, the audit envelope, and the
missing-configfail-loud seam are all real and tested. - The voice speech-to-text path on Telegram was upgraded from a stub to a real
fail-closed seam (
VoiceProviderintelegram/stt-provider.ts); any older doc calling it a stub is stale.
The honest aspirational edge, per the V1 completeness/audit notes:
- End-to-end outbound Telegram delivery in the BFF e2e is unimplemented. The policy core, grounding, webhook auth, and per-message audit are real; the live outbound send loop is the gap.
- WhatsApp, SMS, and payment transports are planners until real provider
credentials are supplied. Their HTTP clients exist (
transports.ts,libs/shared/inbound-integrations/src/payment.ts) and are exercised at deploy time with live keys; with no credentials the seam reportsmissing-configrather than faking a send.
These are explicit, fail-loud absences — never fabricated success.
A note on two stale references#
- The platform architecture doc points readers to
libs/shared/inbound-integrationsfor outbound email/push/SMS adapters. In reality the outbound messaging adapters live inlibs/oshun/messaging-channels/src/{email,push,sms}plustransports.ts(SendGrid, the hand-written SMTP client, Twilio, FCM/APNs/Expo, and Web Push);libs/shared/inbound-integrationsis the inbound connector andpayment.tsside. Look in@oshun/messaging-channelsfor outbound transports. - The registry channel id
email-sesis a naming artifact. The actual email transports aresendEmailViaSendgrid(SendGrid HTTPS API) and the RFC 5321 SMTP client (sendEmailViaSmtp, usable for AWS SES SMTP, Postmark, or Mailpit) — there is no dedicated SES-API transport, and DKIM/SPF/DMARC are deploy-config concerns, not code in this lib. Likewise, the documented push surface is broader than "FCM/APNs": it also covers Expo (push-expo,sendPushViaExpo) and W3C Web Push for PWAs (push-webpush, the full RFC 8030/8291/8292web-push-transport.ts).
Related#
- Telegram Surfaces — Bot, Mini Apps, Channels, Inline, Auth, Payments — the first-class conversational surface in depth.
- WhatsApp, Email, Push, SMS, Discord, and Slack — every thin-channel adapter and its provider gate.
- Sophia Grounding — the grounding the Telegram bot and
Slack
/oshun askenforce before answering. - Lilith Persona Policy — the persona discipline and crisis-handling rules carried onto chat.
- Iris Memory and Identity — the durable-write target the memory-ingestion guard protects.
- Privacy, Consent, Data Portability, and User Controls — consent capture, revocation cascades, and DSAR reach across channels.
- Generation Audience Tiers and Surface Boundaries — the same audience-tier vocabulary on the generation side.
- Architecture, Platform Foundations, and Security — BFF routing, fail-closed posture, and the credential seam.
- Engineering reference: ../architecture/messaging-channels.md.
- Hub: ../features.md. Backlog: §26.1.