# Channel Abstraction, Routing, and Boundaries

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](./telegram-surfaces.md) and
[WhatsApp, Email, Push, SMS, Discord, and Slack](./other-channels.md), 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 a
  `DispatchDecision` of `dispatch` (to a concrete channel) or `suppress` (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](../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](./generation-tiers-and-surfaces.md)
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
`ContentVariant`s, 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:

1. **Tier guard** → `tier-not-allowed-on-messaging`.
2. **Crisis-state suppression.** If `crisisSuppressionActive` and the intent is
   not in `CRISIS_SUPPRESSION_BYPASS` (which contains only `crisis-hotline`) →
   `crisis-suppression`. A crisis flag on the user therefore silences
   non-essential traffic but never blocks the hotline itself.
3. **Quiet hours** → `quiet-hours` (same `crisis-hotline` bypass).
4. **Frequency cap** → `frequency-cap` (same bypass).
5. **Empty tenant allowlist** → `tenant-allowlist-empty`.
6. **Preferred-channel loop.** For each channel in `preferredChannels`, in
   order, the channel must be in the `tenantAllowlist`, in the user's
   `userOptIns`, satisfy the residency requirement (`residencyMatches` —
   `global` matches anything, otherwise an exact match), be `crisisCapable` if
   the intent is `crisis-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 }`.
7. **Diagnosis.** If no channel dispatched, the function works out the precise
   reason: `no-opt-in`, `no-channel-meets-residency`,
   `content-class-not-allowed`, or finally `no-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](./iris-memory-identity.md) and
[Privacy, Consent, Data Portability, and User Controls](./privacy-consent-portability.md).

### 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`, and `post-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's `crisisSuppressionActive` guard 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_FROM` plus a transport: `OSHUN_SMTP_HOST`
  (preferred when set, also used for the Mailpit dev sink, AWS SES SMTP, or
  Postmark SMTP) or `OSHUN_SENDGRID_API_KEY`.
- **SMS** — `OSHUN_TWILIO_ACCOUNT_SID`, `OSHUN_TWILIO_AUTH_TOKEN`,
  `OSHUN_TWILIO_FROM` (optional `OSHUN_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.ts`** exports `sendEmailViaSendgrid` (SendGrid v3
  `api.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`, with
  `buildApnsProviderJwt`), plus `sendSlackViaWebApi` and `sendDiscordViaBot`.
- **`smtp-email-transport.ts`** is a dependency-free **RFC 5321 SMTP client**
  over `node:net` / `node:tls`. It speaks the protocol directly — greeting →
  `EHLO` → optional `STARTTLS` → optional `AUTH` → `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.ts`** implements the W3C Web Push Protocol (RFC 8030)
  with `aes128gcm` payload encryption (RFC 8291 + RFC 8188) and VAPID
  application-server identification (RFC 8292). Because the lib is
  client-transpiled (`node:crypto` is 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** in
  `web-push-transport.test.ts`.
- **`http2-transport-fetch.ts`** exports `createHttp2TransportFetch` — 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`:

- `verifyTelegramMiniAppInitData` derives
  `secret = hmacSha256('WebAppData', botToken)`, then compares the supplied
  `hash` against `hmacSha256Hex(secret, dataCheckString)` in constant time — the
  exact key derivation Telegram specifies for Mini App `initData`.
- `verifyTelegramLoginWidgetPayload` uses `secret = sha256(botToken)` instead,
  per the Login Widget spec.
- Both reject stale (`auth_date` outside `maxAgeSeconds`/skew) and replayed
  (`query_id`/`id` in `consumedIds`) payloads, and `issueTelegramMiniAppSession`
  mints a scoped HS256 JWT (`iss: 'oshun-telegram'`,
  `aud: 'oshun-telegram-miniapp'`) over the allowed scopes `content: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 from `SIDE_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](./sophia-grounding.md) and
[Lilith Persona Policy](./lilith-persona-policy.md) 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](./telegram-surfaces.md).

## 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`}. `planWhatsAppTemplateDelivery` enforces 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 a `CostLedgerEntry` per
  message.
- **SMS** (`sms/index.ts`) — `SmsUseCase` ∈ {`login-code`, `password-reset`,
  `billing-alert`, `crisis-hotline`, `verified-reminder`} (verified-identity
  flows only). `SMS_PRICING_MINOR_USD` is 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`) — `assertSlackEntitlement` requires the workspace
  to be installed, the `metisInstitutionId` to start with `metis-institution:`,
  and all `REQUIRED_SLACK_SCOPES = ['commands', 'chat:write']` to be present.
  `renderSlackOshunCommand` handles `/oshun today|save|ask` over real
  `todayItems` (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`) — `PushPlatform` enum is
  `'fcm-android' | 'fcm-web' | 'apns-ios'` for payload synthesis, while
  `CHANNEL_IDS` additionally carries `push-expo` and `push-webpush` for their
  own transports.

All of these are detailed in
[WhatsApp, Email, Push, SMS, Discord, and Slack](./other-channels.md).

## V3 cross-domain integration

Two modules wire V3 programming into messaging:

- `v3-deep-links.ts` defines `V3_SHARE_LINK_CANONICAL_HOST = 'app.oshun.com'`,
  the custom scheme `oshun` with host `v3` (i.e. `oshun://v3...`), path prefix
  `/v3`, and
  `V3_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.ts` enforces a **30-minute pre-session reminder window**
  (`V3_SESSION_REMINDER_MAX_LEAD_TIME_MINUTES = 30`); a reminder scheduled
  earlier than that returns `outside-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-config` fail-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** (`VoiceProvider` in `telegram/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 reports `missing-config`
  rather 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-integrations` for outbound email/push/SMS adapters. In
  reality the outbound messaging adapters live in
  `libs/oshun/messaging-channels/src/{email,push,sms}` plus `transports.ts`
  (SendGrid, the hand-written SMTP client, Twilio, FCM/APNs/Expo, and Web Push);
  `libs/shared/inbound-integrations` is the **inbound** connector and
  `payment.ts` side. Look in `@oshun/messaging-channels` for outbound
  transports.
- The registry channel id `email-ses` is a naming artifact. The actual email
  transports are `sendEmailViaSendgrid` (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/8292 `web-push-transport.ts`).

## Related

- [Telegram Surfaces — Bot, Mini Apps, Channels, Inline, Auth, Payments](./telegram-surfaces.md)
  — the first-class conversational surface in depth.
- [WhatsApp, Email, Push, SMS, Discord, and Slack](./other-channels.md) — every
  thin-channel adapter and its provider gate.
- [Sophia Grounding](./sophia-grounding.md) — the grounding the Telegram bot and
  Slack `/oshun ask` enforce before answering.
- [Lilith Persona Policy](./lilith-persona-policy.md) — the persona discipline
  and crisis-handling rules carried onto chat.
- [Iris Memory and Identity](./iris-memory-identity.md) — the durable-write
  target the memory-ingestion guard protects.
- [Privacy, Consent, Data Portability, and User Controls](./privacy-consent-portability.md)
  — consent capture, revocation cascades, and DSAR reach across channels.
- [Generation Audience Tiers and Surface Boundaries](./generation-tiers-and-surfaces.md)
  — the same audience-tier vocabulary on the generation side.
- [Architecture, Platform Foundations, and Security](./platform-foundations-and-security.md)
  — BFF routing, fail-closed posture, and the credential seam.
- Engineering reference:
  [../architecture/messaging-channels.md](../architecture/messaging-channels.md).
- Hub: [../features.md](../features.md). Backlog: §26.1.
