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 reportsmissing-configor throws*_not_configuredrather 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 barrelsrc/index.tsre-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, andprovider-config-env. - Apps:
@oshun/telegram-bot(a real grammY app) andapps/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 underapps/oshun/bff/src/customer/andapps/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:
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 isemail-ses, but there is no dedicated SES API transport in the lib. The real email transports aresendEmailViaSendgrid(the SendGrid HTTPS API) andsendEmailViaSmtp(the hand-written RFC 5321 SMTP client, usable against AWS SES SMTP, Postmark, or the dev-stack Mailpit sink). Theemail-sesid 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#
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#
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):
- Tier guard →
tier-not-allowed-on-messaging - Crisis-state suppression →
crisis-suppression - Quiet hours →
quiet-hours - Frequency cap →
frequency-cap - Empty tenant allowlist →
tenant-allowlist-empty - Per-channel pass over
preferredChannels: must be intenantAllowlist, inuserOptIns, satisfy residency (residencyMatches), becrisisCapableif the intent iscrisis-hotline, and allow the content class (channelAllowsContentClass, consultingCHANNEL_BOUNDARIES). The first channel that also yields a feasible variant returns{ kind: 'dispatch', channel, contentClass, chosenVariant }. - If nothing dispatches, a diagnostic tail returns the most specific reason:
no-opt-in,no-channel-meets-residency,content-class-not-allowed, orno-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#
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
AUTH → MAIL FROM → RCPT TO → DATA → QUIT — 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
fetchis HTTP/1.1; APNs requires HTTP/2.src/http2-transport-fetch.tsexportscreateHttp2TransportFetchfor exactly this.provider-config-env.tseven 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:
verifyTelegramMiniAppInitDataderives the secret ashmacSha256('WebAppData', botToken), then compares the suppliedhashagainsthmacSha256Hex(secret, dataCheckString)in constant time.verifyTelegramLoginWidgetPayloadinstead usessecret = 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#
WhatsAppUseCase ∈ receipt, 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#
SmsUseCase ∈ login-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'(originhttps://app.oshun.com), with a custom schemeoshun://v3(V3_SHARE_LINK_CUSTOM_SCHEME = 'oshun',V3_SHARE_LINK_CUSTOM_HOST = 'v3') and path prefix/v3. Shares route overV3_MESSAGING_SHARE_CHANNELS = ['telegram', 'whatsapp', 'push', 'email', 'sms'].buildV3MessagingSharePayloadproduces 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,planV3SessionReminderDeliverysuppresses withoutside-thirty-minute-window. Push provider selection (fcm/apns/expo/webpush) resolves to the rightChannelId.
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,systemCUSTOMER_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-configfail-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). TelegraminitDataverification, 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 theemail-sesid 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.
Related#
- Sophia — Grounding Substrate — the grounder behind Telegram/Mini App grounded answers.
- Iris — Assistant Memory Substrate — the durable-write
boundary that
decideMemoryIngestiondefers to. - Product Surfaces — the seven Studio surfaces the Mini App renders in chat.
- Security, Privacy, and Compliance — consent, residency, retention, and audit posture.
- Support, Entitlements, Billing, and the Aje Entitlement Bridge — the billing/payment context for transactional and billing intents.
V1/features.md§ Messaging Channels and Conversational Surfaces — companion product-scope page.- ../ARCHITECTURE.md — the V1 architecture hub.