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 and
High-Level Architecture, 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. For the security and compliance posture of the audit/identity pieces, see Security, Privacy, and Compliance.
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 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): 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(defaultDEFAULT_EVENT_TTL = 86400seconds, i.e. 24 hours), soreplayUnackedcan 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)andpublish({ delay })both push the event intoscheduledkeyed by its due time; a scheduler loop (defaultDEFAULT_SCHEDULER_INTERVAL = 250ms) pulls due entries and publishes them. The event is durable before the call returns —setTimeoutstate is never the only copy. - Consumer groups are emulated with
SET NXclaim keys. When a subscription declares agroup, members race for a per-(eventId, group)claim key viaSET 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).
removeDeadLetterusesLREMon 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:
- Idempotency is checked first — replays return the cached envelope without touching the domain at all.
- 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.
- The body is validated against its Zod contract.
- The domain adapter commits owned data with tombstone semantics.
- 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.
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 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).
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-typedresultsarray instead ofunknown[].buildPartialFailureEnvelope({ results, errors, partial? })— a builder that computespartialfrom whethererrorsis non-empty and throws if a caller passes apartialflag 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:
// 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 exportsjwt.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, andpassword.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 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 and
Persona, Avatar, and Voice Packs 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
for the payment path and Messaging Channels 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.
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 — contracts, adapters, persistence, OpenAPI, and the shared substrate this page builds on.
- Data Architecture and Tenancy — the tenant, residency, and home-zone model behind the middleware chain.
- High-Level Architecture — where the BFF, domains, and substrates sit.
- Customer-Facing Domains — what lives behind each
/api/oshun/domains/<id>base path. - Security, Privacy, and Compliance — the role model, OAuth/PKCE, and audit posture in context.
- Trust, Safety, and Privacy — the retraction, DSAR, and audit workflows the cascade serves.
- ../ARCHITECTURE.md — the architecture hub.