# Communication Patterns

This page describes how the parts of Oshun V1 talk to each other: the
synchronous request paths between client surfaces, the BFF, and per-domain
services; the asynchronous event bus and job queues that carry cross-domain side
effects; and the shared middleware contracts (idempotency, tenant routing,
residency, the partial-failure envelope, audit) that every write threads
through. It is the "plumbing" companion to [Foundations](./foundations.md) and
[High-Level Architecture](./high-level-architecture.md), and it serves anyone
wiring a new domain route, subscribing to a new topic, or reasoning about what
happens when a write half-succeeds. Everything here maps to real code under
`libs/shared/`, `libs/oshun/`, `libs/contracts/`, `libs/openapi/`, and
`libs/proto/`; where a capability is contracts-level or provider-gated rather
than deployed, that is called out explicitly.

For the data model and tenancy details behind these flows, see
[Data Architecture and Tenancy](./data-architecture-tenancy.md). For the
security and compliance posture of the audit/identity pieces, see
[Security, Privacy, and Compliance](./security-privacy-compliance.md).

---

## Two axes: synchronous request/response and asynchronous fan-out

Oshun separates **synchronous** request/response traffic (a client asks for
something and blocks for the answer) from **asynchronous** event/job traffic (a
write commits, and downstream work happens later, independently, and
idempotently). The reason for the split is blast radius. A customer pressing
"complete session" should get a fast, definitive `200 OK` the moment Tara's data
is committed — they should _not_ wait for Arete to recompute next-practice, for
Iris to update memory, or for analytics to ingest. Those are consequences of the
write, not part of it, so they ride the event bus and the queue substrate where
each consumer fails and retries on its own clock without holding the request
open.

The rest of this page walks both axes in turn, then shows two end-to-end flows —
a generic BFF write and the Veritas claim-retraction cascade — that exercise the
full middleware chain.

---

## Synchronous

### HTTP REST — client ↔ BFF ↔ domain services

The primary synchronous transport is HTTP REST. Client surfaces (the shared
shell, admin web, Oshun Studio) call the **Oshun BFF** (a Fastify
backend-for-frontend), and the BFF calls per-domain adapters. Every request
carries JWT auth, and the BFF applies per-client rate limiting at the edge so a
noisy tenant or a runaway script cannot starve other tenants of capacity.

The contract for every REST surface is an **OpenAPI 3.1** spec, and the specs
are _derived from_ the Zod contracts, never hand-authored against them. Schemas
drive specs (Zod → OpenAPI), and a CI gate fails the build when a spec drifts
from the runtime. The specs live under `libs/openapi/src/specs/`, which contains
**15 real per-domain spec directories** plus the aggregate `main.yaml` and a
`v3.yaml`:

```
arete  bellona  calliope  concordia  hathor  isis  lilith  metis
nisaba  nyx  oshun-bff  sophia  tara  veritas  yemaya
```

Note that this is broader than the customer-facing domain set: it includes
substrate and studio specs (`sophia`, `isis`, `hathor`, `bellona`, `calliope`,
`yemaya`, `concordia`) alongside the six customer domains and the BFF's own
aggregate spec (`oshun-bff`).

Each customer-facing domain is mounted under a stable base path. The
`bff-base-path` values come straight from the domain registry at
`libs/oshun/domain-registry/src/registry.ts` (`OSHUN_DOMAIN_IDS` and
`DOMAIN_REGISTRY`):

| Domain  | BFF base path                |
| ------- | ---------------------------- |
| Tara    | `/api/oshun/domains/tara`    |
| Veritas | `/api/oshun/domains/veritas` |
| Nyx     | `/api/oshun/domains/nyx`     |
| Arete   | `/api/oshun/domains/arete`   |
| Nisaba  | `/api/oshun/domains/nisaba`  |
| Metis   | `/api/oshun/domains/metis`   |

The registry also names `OSHUN_SHELL_PRIMARY_DOMAIN = 'tara'`, which is the
domain the shared shell defaults to when no other primary is configured. See
[Customer-Facing Domains](./customer-domains.md) for what each domain _does_
behind these paths.

### WebSocket — real-time runtime

REST is request/response; some surfaces are inherently streaming. **WebSocket**
is the transport for Psyche, the real-time runtime substrate
([Psyche — Real-Time Runtime Substrate](./substrate-psyche.md)): live
transcripts, voice envelopes, and avatar sync flow over a persistent socket, as
do the Living Scenes _Live Direction Channel_ and Living Activities. These
channels need bidirectional, low-latency frames that a polled REST endpoint
cannot provide.

### gRPC — internal service-to-service

For internal service-to-service calls where latency or streaming matters, Oshun
uses **gRPC** with Protocol Buffers defined under `libs/proto/`. The proto
package is real and broad — it ships a `buf.work.yaml` workspace, a `generated/`
output tree, and roughly two dozen proto domain directories under
`libs/proto/src/`, including `agent`, `ai`, `asset`, `auth`, `bridge`,
`generation3d`, `hathor`, `isis`, `sophia`, `splatting`, `rendering`,
`concordia`, `collaboration`, `pipeline`, `loadbalancing`, `reflection`,
`health`, `common`, `shared`, `user`, `oshun`, `procedural`, `project`, and
`oya`. In practice the highest-traffic gRPC surfaces are the ingestion and
runtime pipelines for **Psyche, Isis, and Sophia**, but the proto surface is not
limited to those three — it spans the generation, rendering, and agent stacks as
well.

---

## Asynchronous

### Event bus — `@oshun/event-bus`

Cross-domain side effects ride the event bus at `libs/shared/event-bus/`. The
public surface is `EventBus` (`export class EventBus implements IEventBus`) plus
the `createEventBus(config: EventBusConfig)` factory, both exported from
`libs/shared/event-bus/src/index.ts`. The pattern is **commit-then-publish**: a
producer commits its owned data first, then publishes a domain event carrying a
correlation ID. Subscribers project the event into their own read models or
workflows, and **idempotency is keyed by `event.id`** so a redelivery is a
no-op.

#### What the substrate actually is — not native Redis Streams

The event bus is built on **`ioredis`** (`import { Redis } from 'ioredis'`) with
two separate connections — one publisher (`pub`) and one subscriber (`sub`),
since Redis pub/sub connections in subscribe mode cannot also issue commands.
But it is **not** built on native Redis Streams. There is no
`XADD`/`XREAD`/`XREADGROUP`/`XGROUP` anywhere in `libs/shared/event-bus/src/`.
Instead, the durability and delivery semantics are assembled from primitive
Redis structures, as documented in the header of
`libs/shared/event-bus/src/event-bus.ts`:

- **Fan-out** is plain Redis **pub/sub**. `publish()` serializes the envelope
  and pushes it to subscribers over a pub/sub channel.
- **Replay source** is a **TTL-bounded key**. The serialized envelope is stored
  under a key with `eventTtl` (default `DEFAULT_EVENT_TTL = 86400` seconds, i.e.
  24 hours), so `replayUnacked` can re-deliver work after a crash-restart. A
  separate Redis HASH tracks which `(eventId, subscriptionId)` /
  `(eventId, group)` pairs have already been acked, so restarts don't re-run
  already-processed work.
- **Delay / nack** uses a durable **sorted set named `scheduled`**.
  `nack(delay)` and `publish({ delay })` both push the event into `scheduled`
  keyed by its due time; a scheduler loop (default
  `DEFAULT_SCHEDULER_INTERVAL = 250` ms) pulls due entries and publishes them.
  The event is durable _before_ the call returns — `setTimeout` state is never
  the only copy.
- **Consumer groups** are emulated with **`SET NX` claim keys**. When a
  subscription declares a `group`, members race for a per-`(eventId, group)`
  claim key via `SET NX`; only the winner runs the handler. Without a group,
  every matching subscription runs — classic broadcast.
- **Dead letter** lives in a Redis **list** (for ordered pagination) plus a
  **hash** (for O(1) id lookup). `removeDeadLetter` uses `LREM` on the
  serialized entry so removal is atomic and sibling ordering is preserved. The
  default dead-letter config is `{ enabled: true, maxEntries: 10000 }`.

So the accurate one-line description is: _the event bus runs on ioredis pub/sub
for fan-out, with a TTL key as the replay source, a `scheduled` sorted set for
delay/nack, lists+hashes for the dead-letter, and `SET NX` claim keys to emulate
consumer groups_ — **not** native Redis Streams. (Older doc copy labeled this
"Redis Streams"; that label was incorrect and has been retired from the V1
docs.)

#### Topic registry and schema versioning

Topics are governed by a **topic registry** (`topic-registry.ts`,
`DEFAULT_EVENT_TOPIC_REGISTRY`) with **schema versioning**. The bus carries a
`registryMode` config: in `'advisory'` mode (the default) it tolerates retired
schemas and falls back to a default schema version; in `'enforced'` mode an
unknown or retired topic is rejected. Each published envelope records the
resolved `schemaVersion`, so consumers can branch on it and producers can evolve
a topic without breaking older subscribers.

#### Outbound delivery and the webhook simulator

Beyond internal fan-out, the same library handles **outbound** webhook delivery
to tenant integrations via `outbound-delivery.ts`, whose surface includes
`OutboundSigningKey`, `OutboundSignatureHeaders`, `SignOutboundEventInput`,
`OutboundDeliveryTransport`, `OutboundDeliveryDeadLetter`, and
`OutboundEventAuditRecord` — i.e. signed payloads, retry/backoff, a dead-letter
for failed deliveries, replay, and a per-event audit record. A
`webhook-simulator.ts` lets a tenant's integration sandbox exercise these
deliveries before going live.

### Job queues — `@oshun/queue`

Long-running work — editorial, asset processing, agentic-AI runs, notifications,
billing, integration syncs — runs on the durable queue substrate at
`libs/shared/queue/`. The files are exactly the substrate the docs claim:
`durable-queue.ts` (priority classes, replay, deduplication),
`dead-letter-queue.ts`, `sla-monitor.ts` (per-job-class SLA monitoring),
`memory-queue.ts` (an in-memory implementation for tests/local), and
`worker.ts`. A single shared substrate is consumed by every domain that needs
background work, which keeps replay/DLQ/SLA behavior uniform instead of leaving
each team to reimplement it.

The two async substrates are complementary: the **event bus** carries small,
fan-out _facts_ ("this happened"), and the **queue** carries large or slow
_units of work_ ("do this"). A producer often publishes an event, and a
subscriber turns that event into a queued job.

---

## The request lifecycle through the BFF

Every customer- or operator-issued write passes the same sequence of middlewares
before it reaches a domain adapter. The ordering is deliberate so that the
cheap, short-circuiting checks run first:

1. **Idempotency** is checked first — replays return the cached envelope without
   touching the domain at all.
2. **Tenant** and **residency** resolve next, so the rest of the pipeline can
   short-circuit on policy (wrong region, missing cross-region consent) before
   any validation or write.
3. The body is validated against its **Zod contract**.
4. The **domain adapter** commits owned data with tombstone semantics.
5. A **domain event** is published and an **audit record** appended.

The building blocks for every stage exist as real code (idempotency middleware,
tenant context, residency enforcer, Zod contracts, domain adapters, event bus,
audit platform). The diagram below describes that intended composition. No
single production BFF route has been traced to prove that every middleware fires
in exactly this order in the live wiring, so treat the _ordering_ as the design
intent rather than a verified end-to-end trace.

```mermaid
sequenceDiagram
    autonumber
    actor C as Client
    participant BFF as Oshun BFF
    participant Idem as Idempotency Cache
    participant T as Tenant Middleware
    participant R as Residency Enforcer
    participant V as Zod Contract Validator
    participant D as Domain Adapter
    participant DB as Domain DB
    participant EB as Event Bus
    participant A as Audit Platform

    C->>BFF: POST /api/oshun/domains/X<br/>Idempotency-Key · X-Tenant-ID · JWT
    BFF->>Idem: lookup(idempotencyKey)
    alt cache hit
        Idem-->>BFF: stored response
        BFF-->>C: 200 OK (replayed)
    else cache miss
        BFF->>T: resolve tenant + scopes
        T-->>BFF: tenant context · homeZone
        BFF->>R: check residency vs request region
        R-->>BFF: allow / require consent
        BFF->>V: validate body
        V-->>BFF: parsed payload
        BFF->>D: invoke(operation, payload)
        D->>DB: write owned data (tombstone-aware)
        DB-->>D: ack
        D->>EB: publish domain event<br/>{correlationId}
        D->>A: append mutation record
        D-->>BFF: result
        BFF->>Idem: store(key, response)
        BFF-->>C: 200 OK
    end
```

### Stage details, grounded in code

**Idempotency** comes from `libs/shared/http-client/src/idempotency.ts`. The
header name is `DEFAULT_IDEMPOTENCY_HEADER = 'Idempotency-Key'`, the default
retention is `DEFAULT_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000` (24 hours), and
the machinery is the `IdempotencyMiddleware` class with a
`createIdempotencyMiddleware()` factory. This is one of several cross-cutting
HTTP concerns in the same library: the sibling modules `circuit-breaker.ts`,
`ssrf-guard.ts`, `retry.ts`, `tenant-context.ts`, and `tracing.ts` provide
circuit-breaking, SSRF protection, retry/backoff, tenant propagation, and
distributed tracing, respectively — the BFF and inter-service callers compose
these rather than reinventing them.

**Tenant and residency** are enforced by `libs/shared/data-residency/`, whose
modules are `enforcer.ts`, `home-zone.ts`, `traffic-shaping.ts`, and
`dsr-routing.ts`. Tenant-aware middleware reads `X-Tenant-ID` (or the token
claim) and propagates it downstream; the residency layer resolves the user's
**home zone** and shapes traffic toward their primary data plane unless explicit
cross-region consent is present. See
[Data Architecture and Tenancy](./data-architecture-tenancy.md) for the full
tenancy model.

**Validation** is the Zod contract for the operation (one source per object
class, each with a round-trip fixture; see [Foundations](./foundations.md)).

**The adapter** writes with **tombstone semantics** — deletions are soft
tombstones that propagate and are audit-logged, never silent re-creation — and
the persistence layer enforces this at `libs/oshun/persistence/`. That library
renders Prisma from the contracts (`prisma-renderer.ts`, ~11.8 KB;
`zod-prisma-introspection.ts`, ~5.5 KB) and guards correctness with a quartet of
tests that runs in CI: `contract-persistence-registry.test.ts` (Zod↔persistence
drift), `index-requirements.test.ts` (per-table required indexes),
`migration-plan.test.ts` (idempotent, dry-runnable migrations), and
`tombstone-semantics.test.ts`. It also ships `dsar-deletion-cascade.ts` and
`dsar-erasure-runtime.ts` for privacy deletions — the runtime side of the
cascade flows shown below.

**Event and audit** complete the write: the event goes to `@oshun/event-bus`,
and the audit record is appended to `libs/shared/audit-platform/` (see below).

---

## The partial-failure envelope contract

When an operation fans out over several items or several domains and some
succeed while others fail, the response cannot honestly be a single success or
single error. Oshun standardizes this case with a **first-class Zod contract**
at `libs/contracts/src/common/partial-failure-envelope.ts` — not just an
informal shape. The exported surface is:

- `PartialFailureEnvelopeSchema` — the concrete schema, shape
  `{ results: unknown[], errors: PartialFailureError[], partial: boolean }`.
- `PartialFailureErrorSchema` / `PartialFailureError` — each error is
  `{ domain: string, stage?: string, message: string }` (so a consumer can see
  _which domain_ and _which stage_ failed, not just a flat message).
- `createPartialFailureEnvelopeSchema(resultSchema, errorSchema?)` — a generic
  factory that produces a typed envelope for a specific result schema (and
  optionally a richer error schema), so each caller gets a strongly-typed
  `results` array instead of `unknown[]`.
- `buildPartialFailureEnvelope({ results, errors, partial? })` — a builder that
  computes `partial` from whether `errors` is non-empty and **throws** if a
  caller passes a `partial` flag that contradicts the actual error count.
- `isPartialFailureEnvelope(value)` — a structural type guard.

The contract enforces a cross-field invariant via `superRefine`, so an
inconsistent envelope is rejected at the boundary:

```ts
// from partial-failure-envelope.ts (cross-field refinement)
if (value.partial && value.errors.length === 0) {
  // "partial=true requires at least one error entry."
}
if (!value.partial && value.errors.length > 0) {
  // "partial=false requires an empty errors array."
}
```

In other words: `partial === true` ⇔ `errors.length >= 1`. You cannot claim a
partial failure with no errors, nor claim full success while reporting errors.
This is the contract behind the informal
`{ results[], errors[], partial: true }` shape that older copy describes — and
it pairs with the retry/dedup/replay/dead-letter contract tests that ride on
every job submitter.

---

## Identity, roles, and the platform-foundations package

Authentication and authorization sit underneath every synchronous and
asynchronous path. There are three real homes for this:

- **`libs/shared/auth-primitives/`** is richer than "JWT and session
  primitives." It exports `jwt.ts`, `session.ts`, `api-key.ts`,
  `oauth-client.ts`, `oauth-revoke.ts`, `token-refresh.ts`, `token-audit.ts`,
  `totp.ts` (TOTP / step-up MFA), `platform-roles.ts`, `tenant-isolation.ts`,
  and `password.ts` — the full set of token, key, MFA, role, and
  tenant-isolation primitives.
- **`libs/oshun/auth/`** is published as **`@oshun/auth-client`**, the
  workspace-side identity and session client every Oshun app consumes.
- **`libs/shared/identity/`** holds RBAC and permission checks.

### The `@oshun/platform-foundations` package

The platform-level identity, public-API, and operational subsystems are
consolidated in one package, `libs/oshun/platform-foundations/`. Its
`src/index.ts` re-exports exactly **nine** subsystems, each in its own
directory:

| Subsystem           | Concern                                                |
| ------------------- | ------------------------------------------------------ |
| `service-discovery` | Locating peer services.                                |
| `public-api`        | The external API platform, including OAuth 2.1 / PKCE. |
| `shared-contracts`  | Cross-cutting platform contracts.                      |
| `role-model`        | The canonical role + scope model (below).              |
| `step-up`           | Step-up authentication for sensitive operations.       |
| `secrets`           | Secret management.                                     |
| `configs`           | Platform configuration.                                |
| `rollback`          | Rollback controls.                                     |
| `abuse-controls`    | Abuse / rate / anomaly controls.                       |

### OAuth 2.1 / PKCE (`§27`)

`libs/oshun/platform-foundations/src/public-api/oauth.ts` is the "§27 Public API
platform: OAuth 2.1/PKCE" module. It defines
`TOKEN_TYPES = ['access', 'refresh']`, the PKCE code-challenge pattern
`PKCE_CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43,128}$/`, and a request
validator (`validateAuthorizationCodeRequest`) whose error codes include
`'pkce-required'` and `'pkce-verification-failed'` alongside
`'client-mismatch'`, `'invalid-redirect-uri'`, and `'scope-not-allowed'`. This
is **contracts / state-machine level**: it encodes the token types, PKCE
patterns, scope checks, and refresh/revocation surface. It is _not_ a verified
live authorization-server deployment — treat the OAuth surface as the validated
protocol logic rather than a running auth server.

### The canonical role + scope model (`§28`)

`libs/oshun/platform-foundations/src/role-model/role-model.ts` defines
`CANONICAL_ROLES` with **ten** entries — more numerous, and more precisely
named, than the prose elsewhere ("support", "admin leadership") suggests:

| Canonical role     | Sample scopes (`scopesFor(role)`)                                                    |
| ------------------ | ------------------------------------------------------------------------------------ |
| `customer`         | `customer.self`, `customer.shell`                                                    |
| `creator`          | `customer.self`, `customer.shell`, `creator.studio`                                  |
| `support-agent`    | `support.case.read`, `support.case.write`                                            |
| `reviewer`         | `review.queue.read`, `review.queue.decide`                                           |
| `moderator`        | `review.queue.read`, `review.queue.decide`, `moderation.decide`                      |
| `privacy-operator` | `privacy.dsar.execute`, `privacy.audit.read`, `admin.audit.global`                   |
| `model-operator`   | `model.registry.read`, `model.registry.promote`                                      |
| `persona-operator` | `persona.registry.read`, `persona.registry.publish`                                  |
| `tenant-admin`     | `tenant.console`, `admin.audit.global`                                               |
| `admin-leadership` | `admin.users.act`, `admin.scope.grant`, `admin.breakglass.act`, `admin.audit.global` |

Note the canonical names are precise (`support-agent`, not "support") and the
set includes roles that looser prose omits, notably the distinct `creator` and
`tenant-admin` roles. The library also exports `SCOPE_KEYS` (the 21-entry scope
vocabulary above plus `admin.policy.write`, `admin.config.write`),
`scopesFor(role)`, and `filterFieldsByRole()` for least-privilege field
projection. See
[Security, Privacy, and Compliance](./security-privacy-compliance.md) for how
these roles gate operator surfaces.

---

## The audit platform

Every mutation that matters lands in the append-only audit substrate at
`libs/shared/audit-platform/` — and this is a substantial real library (~40
implementation modules), not a single helper. Its tamper-evidence core is
`hash-chain.ts`, which chains entries so an auditor holding any past `entryHash`
can detect insertion, deletion, or mutation by replaying forward:

```
entryHash    = sha256(previousChainHash || eventHash)
eventHash    = sha256(canonicalSerialize(event))
```

Around that core sit `schema-versioning.ts` (versioned audit-event schemas),
`retention.ts`, `escalation-rules.ts`, `compliance-attestation.ts`,
`watermark-verification.ts`, `synthetic-media-labeling.ts`,
`voice-likeness-consent.ts`, and `provenance-bundle-validation.ts`, among
others. The platform backs Iris admin inspection, DSAR review, the deletion
workflow, and review packages — see
[Trust, Safety, and Privacy](./trust-safety-and-privacy.md) and
[Persona, Avatar, and Voice Packs](./persona-avatar-voice-packs.md) for the
synthetic-media and voice-consent surfaces it underpins.

---

## Inbound integration plumbing

Inbound connectors — the way institutional systems push data _into_ Oshun — live
at `libs/shared/inbound-integrations/`. The `src/index.ts` barrel exports
**fifteen** connector modules (the customer-facing table below historically
under-counted them at nine). The substantial ones are large, real
implementations: `lms.ts` is ~1,425 LOC, `oneroster.ts` ~1,081 LOC, and
`identity.ts` ~1,048 LOC.

| Connector family | Module(s)                                                            | V1 use                                                                                                                                                                                                           |
| ---------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| LMS              | `lms.ts`, `lti-verification.ts`, `scorm-rte.ts`, `scorm-2004-rte.ts` | LTI 1.3 / Advantage, LTI launch verification, and two SCORM runtime (RTE) implementations for Metis institutions.                                                                                                |
| OneRoster        | `oneroster.ts`                                                       | Rostering sync with conflict reporting and dry-run.                                                                                                                                                              |
| Identity         | `identity.ts`                                                        | SAML 2.0, OIDC, SCIM 2.0 for tenant SSO.                                                                                                                                                                         |
| Calendar         | `calendar.ts`, `calendar-google-transport.ts`                        | Google/Apple/Outlook two-way sync for Tara/Arete/Nyx/Metis, with a dedicated Google transport.                                                                                                                   |
| Payment (fiat)   | `payment.ts`                                                         | Fiat-rail entitlement upgrades (Stripe-class providers; Telegram payments). V1.x optional — the V1 primary path is non-custodial crypto via the **Aje** domain (`libs/aje/`) plus `libs/oshun/payments-bridge/`. |
| Telemetry        | `telemetry.ts`                                                       | xAPI / cmi5 / Caliper export to institutional sinks.                                                                                                                                                             |
| BYOM             | `byom.ts`, `byom-model.ts`                                           | Tenant-provided model endpoints for Metis study; `byom-model.ts` is a distinct model-description module.                                                                                                         |
| Notification     | `notification.ts`                                                    | Slack / Teams notification sinks for institutional surfaces.                                                                                                                                                     |
| Health           | `health.ts`                                                          | Connector health probes, circuit breakers, version pinning.                                                                                                                                                      |
| (shared)         | `types.ts`                                                           | Shared connector types.                                                                                                                                                                                          |

The expansion over the old nine-row view is concrete: `lti-verification.ts` (LTI
launch verification), **two** SCORM RTEs (`scorm-rte.ts` and
`scorm-2004-rte.ts`), `byom-model.ts` separate from `byom.ts`, and
`calendar-google-transport.ts` separate from `calendar.ts`. See
[Support, Entitlements, Billing, and the Aje Entitlement Bridge](./support-billing-and-crypto.md)
for the payment path and [Messaging Channels](./messaging-channels.md) for the
notification sinks.

---

## Cross-domain event examples (V1)

The event bus is what lets domains stay isolated while still reacting to each
other. These are representative V1 topics, each with its producer and consumers:

| Topic                                 | Producer | Consumer(s)                                   |
| ------------------------------------- | -------- | --------------------------------------------- |
| `tara.session.completed`              | Tara     | Arete (next-practice), Iris memory, analytics |
| `arete.weekly_review.published`       | Arete    | Shell continuity, assistant, Iris memory      |
| `veritas.claim.retracted`             | Veritas  | Sophia (evidence invalidation), shell banner  |
| `nyx.event.upcoming`                  | Nyx      | Calendar sync, shell continuation, Tara cue   |
| `nisaba.notebook.entry_added`         | Nisaba   | Assistant context, search index               |
| `metis.assessment.evidence_collected` | Metis    | Themis adjudication, Sophia grounding         |
| `iris.consent.changed`                | Iris     | All domains (memory scope re-eval), audit     |
| `isis.generation.bundle_ready`        | Isis     | Studio, Living Scenes, audit, provenance      |
| `lilith.crisis_frame.activated`       | Lilith   | Tara (suppress invitations), assistant, Isis  |

Each topic name is a versioned entry in the topic registry, and each consumer
dedupes on `event.id`, so a redelivery after a crash is harmless.

---

## Example flow — Veritas claim retraction cascade

A retraction is a high-blast-radius write: the source-of-truth domain commits
the tombstone, the event bus fans out, and downstream subscribers project the
retraction into their own surfaces. Producer and subscribers are isolated by the
bus; failures replay until each projection succeeds. This is exactly the pattern
the partial-failure envelope and tombstone semantics exist to support — the
retraction commits atomically in Veritas, and the _consequences_ (Sophia
evidence invalidation, the customer banner, the audit record) are independent,
idempotent projections.

```mermaid
sequenceDiagram
    autonumber
    actor Op as Operator
    participant Admin as Admin Web
    participant BFF as Oshun BFF
    participant V as Veritas
    participant DB as Veritas DB
    participant EB as Event Bus<br/>(ioredis pub/sub)
    participant S as Sophia
    participant Sh as Shell (Customer)
    participant A as Audit Platform

    Op->>Admin: Approve retraction (claim, reason)
    Admin->>BFF: POST /api/oshun/domains/veritas/claims/:id/retract
    BFF->>V: retract(claimId, reason, actor)
    V->>DB: Write tombstone + CorrectionNote<br/>+ RetractionCascade entry
    V->>EB: publish veritas.claim.retracted<br/>{correlationId, claimId, severity}
    V-->>BFF: 200 OK
    BFF-->>Admin: 200 OK

    par Evidence invalidation
        EB-->>S: deliver event
        S->>S: Invalidate evidence packs<br/>citing claimId
        S->>EB: publish sophia.evidence.invalidated
    and Customer banner
        EB-->>Sh: deliver event
        Sh->>Sh: Render retraction banner on<br/>Story / TopicHub / linked notebooks
    and Audit
        EB-->>A: deliver event
        A->>A: Append immutable hash-chain record<br/>(actor, reason, correlationId)
    end

    Note over EB,A: Subscribers idempotent by event.id — failures replay from the TTL-bounded replay key, not native Streams.
```

The POST route uses the registry base path `/api/oshun/domains/veritas`; the
tombstone is written through the persistence layer's mandatory tombstone
semantics; the audit append lands in the `hash-chain.ts` log; and the bus's
replay source is the **TTL-bounded replay key and `scheduled` sorted set**, not
a native Redis Stream (the older diagram's "Redis Streams" participant label is
corrected above).

---

## Related

- [Foundations](./foundations.md) — contracts, adapters, persistence, OpenAPI,
  and the shared substrate this page builds on.
- [Data Architecture and Tenancy](./data-architecture-tenancy.md) — the tenant,
  residency, and home-zone model behind the middleware chain.
- [High-Level Architecture](./high-level-architecture.md) — where the BFF,
  domains, and substrates sit.
- [Customer-Facing Domains](./customer-domains.md) — what lives behind each
  `/api/oshun/domains/<id>` base path.
- [Security, Privacy, and Compliance](./security-privacy-compliance.md) — the
  role model, OAuth/PKCE, and audit posture in context.
- [Trust, Safety, and Privacy](./trust-safety-and-privacy.md) — the retraction,
  DSAR, and audit workflows the cascade serves.
- [../ARCHITECTURE.md](../ARCHITECTURE.md) — the architecture hub.
