Oshun Platform · Architecture

High-Level Architecture

A focused page within the Oshun Platform Architecture documentation. The full map and every sibling page live in the Architecture hub.

8sections12 minread1diagram4tables

On this page

This page is the one-screen mental model of Oshun V1: how a request travels from a client surface, through the shared shell and the single BFF, into the customer-facing domains, down through the platform substrates and the shared foundations, and finally to the data and runtime stores. It serves engineers and platform leads who need to know which box a thing lives in before opening any single subsystem page, and it sits directly above the per-subsystem pages — Product Surfaces, Customer-Facing Domains, the substrate pages, and Foundations — that zoom into each layer. It is part of the V1 architecture set hubbed at ../ARCHITECTURE.md.

The V1 Stack at a Glance#

V1 is a strict subset of the wider Oshun monorepo: six customer-facing domains (Tara, Veritas, Nyx, Arete, Nisaba, Metis), six platform substrates (the five product substrates Sophia, Iris, Psyche, Lilith, Isis, plus the Aje payment substrate), six cross-domain support subsystems (Aja, Yemaya, Themis, plus the Studio-only Bellona, Hathor, Neith), a set of operator and distribution surfaces, and the foundations they all share. Everything below is wired so that exactly one path exists from any client to any domain: there is a single shell, a single BFF, a single contracts package, and one typed adapter per substrate.

flowchart TB subgraph CLIENTS["Client Surfaces"] direction LR web["Customer Web"] mobile["Customer Mobile"] adminWeb["Admin Web"] adminMobile["Admin Mobile"] web ~~~ mobile ~~~ adminWeb ~~~ adminMobile end shell["<b>Shared Consumer Shell</b><br/><sub>@oshun/shell-* · navigation · offline · ui · design-tokens</sub>"] bff["<b>Oshun BFF</b> <sub>· apps/oshun/bff</sub><br/><sub>Fastify · OpenAPI 3.1 · Idempotency · Tenant · Residency</sub><br/><sub>Routes /api/oshun/domains/{tara, veritas, nyx, arete, nisaba, metis}</sub>"] subgraph DOMAINS["Customer-Facing Domains · libs/oshun/domain-*"] direction LR tara[Tara] veritas[Veritas] nyx[Nyx] arete[Arete] nisaba[Nisaba] metis["Metis<br/><sub>apps/metis/*</sub>"] tara ~~~ veritas ~~~ nyx ~~~ arete ~~~ nisaba ~~~ metis end subgraph SUBSTRATES["Platform Substrates"] direction LR sophia["<b>Sophia</b><br/><sub>evidence-sophia</sub>"] iris["<b>Iris</b><br/><sub>memory-iris</sub>"] psyche["<b>Psyche</b><br/><sub>embodiment-psyche</sub>"] lilith["<b>Lilith</b><br/><sub>persona-policy-lilith</sub>"] isis["<b>Isis</b><br/><sub>generation-control-isis</sub>"] aje["<b>Aje</b><br/><sub>libs/aje · payments-bridge</sub>"] sophia ~~~ iris ~~~ psyche ~~~ lilith ~~~ isis ~~~ aje end subgraph FOUNDATIONS["Foundations · Shared Infrastructure"] direction LR contracts["@oshun/<br/>contracts"] persistence["@oshun/<br/>persistence"] eventbus["@oshun/<br/>event-bus"] queue["@oshun/<br/>queue"] integrations["@oshun/inbound-<br/>integrations"] residency["@oshun/<br/>data-residency"] identity["@oshun/<br/>identity"] audit["@oshun/<br/>audit-platform"] contracts ~~~ persistence ~~~ eventbus ~~~ queue queue ~~~ integrations ~~~ residency ~~~ identity ~~~ audit end subgraph DATA["Data &amp; Runtime"] direction LR pg[("PostgreSQL<br/>+ pgvector")] redis[("Redis<br/>Streams · cache")] minio[("MinIO / S3")] es[("Elasticsearch")] qdrant[("Qdrant")] neo4j[("Neo4j")] kafka[("Kafka")] otel{{"OTLP · Jaeger<br/>Prometheus · Grafana"}} pg ~~~ redis ~~~ minio ~~~ es es ~~~ qdrant ~~~ neo4j ~~~ kafka ~~~ otel end CLIENTS --> shell shell -->|HTTPS + WebSocket| bff bff --> DOMAINS DOMAINS --> SUBSTRATES SUBSTRATES --> FOUNDATIONS FOUNDATIONS --> DATA classDef tier fill:#fef3c7,stroke:#b45309,color:#1f2937 classDef substrate fill:#dbeafe,stroke:#1e40af,color:#1e3a8a classDef found fill:#ecfeff,stroke:#0e7490,color:#083344 classDef store fill:#f3e8ff,stroke:#6d28d9,color:#3b0764 class tara,veritas,nyx,arete,nisaba,metis tier class sophia,iris,psyche,lilith,isis,aje substrate class contracts,persistence,eventbus,queue,integrations,residency,identity,audit found class pg,redis,minio,es,qdrant,neo4j,kafka,otel store

The diagram is layered top-to-bottom, and the layering is load-bearing: each layer talks only to the one directly beneath it. Client surfaces never call the BFF directly — they call it through the shared shell. Domains never reach into data stores directly — they go through substrates and foundations. This is what keeps the six domains swappable and the substrate runtimes hidden behind stable shapes.

The Five Architectural Commitments#

V1 leans on five decisions that the rest of the document elaborates. They are the reason the stack reduces to a single readable diagram instead of a mesh.

1. A single BFF fronts every Oshun-side domain#

There is exactly one Backend-for-Frontend at apps/oshun/bff (package @oshun/bff), built on Fastify with OpenAPI 3.1, idempotency keys, tenant scoping, and data-residency enforcement. Every customer-facing domain is reached through one BFF prefix, /api/oshun/domains/<domain>. These prefixes are not folklore — they are declared as the single source of truth in libs/oshun/domain-registry/src/registry.ts, where each domain entry carries a bff-base-path:

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 order — tara, veritas, nyx, arete, nisaba, metis — is the canonical OSHUN_DOMAIN_IDS ordering exported from the same file, and it is the ordering the BFF, the shell, and the navigation layer all honor. Because the BFF is singular, cross-cutting concerns (auth, idempotency, tenant isolation, residency routing, rate limits, audit) are implemented and tested once rather than per surface. See Communication Patterns for the request envelope and Data Architecture and Tenancy for residency and isolation.

2. A single contracts package drives every spec#

@oshun/contracts at libs/contracts/ holds the Zod schemas that define every request and response shape. Those schemas are not duplicated into hand-written OpenAPI — they generate the OpenAPI specs under libs/openapi/src/specs/. The practical consequence is that the BFF, the typed domain adapters, the admin surfaces, and the assistant all share one definition of truth: change a schema in one place and the spec, the types, and the contract tests move with it. Each domain keeps its own contracts under libs/contracts/src/<domain>/ (Zod schemas plus spec tests), so the single package is internally partitioned by domain.

3. Per-domain adapters present typed read APIs to the shell#

Under libs/oshun/domain-*, each domain ships an adapter that exposes a typed read API to the shared shell, the admin surfaces, and the assistant. The shell never imports a domain's internal runtime — it imports the adapter's stable shape. This is the boundary that lets a domain rework its internals without forcing a shell change, and it is why the shell can render all six domains with one composition strategy.

4. Substrate adapters hide the substrate runtimes behind one shape#

Each of the platform substrates is consumed through a thin Oshun-side adapter whose only job is to present one stable shape and hide the underlying runtime:

Substrate Adapter package What it hides
Sophia evidence-sophia grounding / evidence runtime
Iris memory-iris assistant memory + continuation state
Psyche embodiment-psyche real-time runtime
Lilith persona-policy-lilith contemplative persona policy
Isis generation-control-isis generation control
Aje libs/aje/ via payments-bridge non-custodial settlement

The Aje payment substrate (libs/aje/) is consumed specifically through the Oshun-side bridge libs/oshun/payments-bridge/ so that settlement stays non-custodial and the rest of the platform sees only an entitlement-shaped interface. Each substrate has its own deep-dive: Sophia, Iris, Psyche, Lilith, Isis, and Aje.

5. Metis runs as its own stack, integrated through the shell#

Metis is the launch-blocking education product and the only V1 domain with its own dedicated apps and microservice stack beneath apps/metis/. The Metis runtime is apps/metis/{web,admin,api-gateway,worker} and is integrated into the Oshun shell through libs/metis/api-client (renamed under the Oshun adapter convention per V1/TODOS.md § 1.3). Every other customer-facing domain renders through apps/oshun/web and apps/oshun/mobile via the shared shell and the BFF; Metis is the exception that brings its own backend.

A nuance the surface table flattens: Metis is not solely rendered through apps/oshun/mobile. There is also a separate Metis mobile app at apps/metis/mobile. So while every other domain is mobile-rendered only through the Oshun mobile shell, Metis has both an Oshun-shell presence and its own dedicated Metis mobile surface. The apps/metis/ tree therefore contains web, admin, api-gateway, worker, and mobile.

How a Request Actually Flows#

The arrows in the diagram describe a concrete journey. Walk through a customer opening a Veritas claim from the web:

  1. Client → Shell. The customer is in apps/oshun/web. They click a claim card. The click is handled inside the shared shell, not by a bespoke page — the shell owns navigation, current-domain persistence, and command-surface registration via @oshun/navigation.
  2. Shell → BFF. The shell issues an HTTPS request (and, for live work, a WebSocket) to the single BFF. It hits the Veritas prefix /api/oshun/domains/veritas, carrying the tenant, residency, and idempotency context the BFF enforces.
  3. BFF → Domain. The BFF dispatches to the Veritas domain adapter under libs/oshun/domain-*, validated against the Zod contracts in libs/contracts/src/veritas/.
  4. Domain → Substrates. The Veritas domain pulls grounding through the Sophia adapter (evidence-sophia), assistant context through Iris (memory-iris), and so on — each substrate behind its single stable shape.
  5. Substrates → Foundations → Data. The substrates persist and read through the foundations (@oshun/persistence, @oshun/event-bus, @oshun/queue, @oshun/data-residency, @oshun/identity, @oshun/audit-platform, @oshun/inbound-integrations), which finally touch PostgreSQL+pgvector, Redis, MinIO/S3, Elasticsearch, Qdrant, Neo4j, and Kafka, with OTLP/Jaeger/Prometheus/Grafana observing the whole path.

The same five-hop shape holds for every domain; only the prefix and the adapter change. That uniformity is the payoff of the single-BFF, single-contracts design.

Client Surfaces Feeding the Top Layer#

The CLIENTS box in the diagram contains four boxes, but the actual surface inventory is larger and worth naming precisely, because two real surfaces are easy to miss.

The named app surfaces (all real, all packaged)#

Every one of these is a real workspace package with its own package.json and a substantial src/:

Surface Path Package
Customer Web apps/oshun/web/ @oshun/web
Customer Mobile apps/oshun/mobile/ @oshun/mobile
Oshun BFF apps/oshun/bff/ @oshun/bff
Admin Web apps/oshun/admin/ @oshun/admin
Admin Mobile apps/oshun/admin-mobile/ @oshun/admin-mobile
Tenant Console apps/oshun/tenant-admin/ @oshun/tenant-admin
Telegram Bot apps/oshun/telegram-bot/ @oshun/telegram-bot
Telegram Mini App apps/oshun/telegram-miniapp/ @oshun/telegram-miniapp

Two surfaces the prose has historically under-described#

  • Content serviceapps/oshun/content-service/ is a real app, package @oshun/content-service-app. The Product Surfaces table has historically omitted this surface; it belongs in the inventory.
  • Legalapps/oshun/legal/ is not an app. It is a markdown-only surface containing privacy-policy.md and terms-of-service.md with no package.json. The architecture's references to "the legal surface at apps/oshun/legal/" are correct, but it is a docs folder, not a buildable app, and should be read that way. See Security, Privacy, and Compliance for how these documents are consumed.

For the web app, the PWA layer is real and test-covered, not aspirational. The service worker lives at apps/oshun/web/public/sw.js, the manifest at apps/oshun/web/public/manifest.json (name: "OSHUN", start_url: "/?surface=pwa", display: "standalone", theme_color: "#f1ebdd"), and the install/update/caching logic at apps/oshun/web/src/lib/pwa-service-worker.ts with its policy test at apps/oshun/web/src/lib/pwa-service-worker.test.ts.

The mobile app is a real Expo + Expo Router project: app.json sets expo.scheme: "oshun", the iOS bundleIdentifier is com.oshun.mobile, and its associatedDomains include applinks:oshun.app (plus www.oshun.app, app.oshun.com, and activitycontinuation:/webcredentials: entries). Its tab IA lives under apps/oshun/mobile/app/(tabs)/ as index, explore, activity, library, and profile. Admin Mobile (@oshun/admin-mobile) is likewise a full Expo Router app with an app/(operator)/ group plus login.tsx and step-up.tsx, and a deep src/ (auth, incidents, offline, review, urgent-queue, and more). See Product Surfaces for the full surface walkthrough.

The Shared Shell Is Wider Than the Diagram Label#

The shell subtitle in the diagram reads @oshun/shell-* · navigation · offline · ui · design-tokens, and the @oshun/shell-* glob is doing more work than the commonly cited composition list implies. The shell is genuinely composed from these real libraries:

Shell library Role
@oshun/shell-core home / explore / activity / library / notifications / profile / settings / assistant entry points
@oshun/shell-assistant collapsible assistant dock + context handoff
@oshun/shell-routines cross-domain routine and continuation cards
@oshun/shell-wearable watch / widget / Live-Activity-style surface
@oshun/shell-desktop desktop (Electron-class) surface — see below
@oshun/shell-achievements achievements, challenges, social accountability — see below
@oshun/navigation current-domain persistence, deep links, route analytics, command-surface registration
@oshun/offline offline/PWA/mobile sync substrate — see below
@oshun/design-tokens, @oshun/ui design system + component library

Three of these are real shell libraries that the older composition prose did not name, and they materially change the picture:

  • @oshun/shell-desktop is a whole desktop surface class — an Electron-class companion. Its src/ carries desktop-engine.ts, window-manager.ts, tray-companion.ts, protocol-handler.ts, update-manager.ts, widget-engine.ts, notification-bridge.ts, and shortcut-manager.ts. This is a surface category beyond web and mobile; it is real code, and it should be understood as part of the shell composition.
  • @oshun/shell-achievements ships achievement-engine.ts, achievement-definitions.ts, challenge-templates.ts, and social-accountability.ts — the engagement/accountability layer that rides on top of the cross-domain shell.
  • @oshun/offline is the real PWA/mobile offline substrate that the shell composition list also tends to drop. Its src/ exports an OfflineSyncQueue class (queue.ts), an OfflineCache class (cache.ts), and the SyncQueueItem, RetryPolicy, and ConnectivityState types (types.ts), plus storage.ts, retry.ts, and connectivity.ts. It is the substrate behind the web PWA's offline shell and the mobile app's sync behavior.

See the Shared Consumer Shell section of Product Surfaces and the Subsystem Glossary for the per-library detail.

The /domains/* Namespace Covers All Six Domains#

Customer web ships two coordinated namespaces per domain: a presentational consumer hub at /<domain> and a power-user deep-tools namespace at /domains/<domain>/*. An earlier reading of the docs claimed Nisaba and Metis ship no parallel /domains/* namespace and instead bolt their deep tools onto the consumer hub. The code disagrees, and the code is authoritative.

Under apps/oshun/web/src/app/domains/ there are four static domain directories — arete, nyx, tara, veritas — plus a [domainId] dynamic catch-all and a layout.tsx. The catch-all is what extends the namespace to the remaining two domains:

  • apps/oshun/web/src/app/domains/[domainId]/page.tsx carries a DOMAIN_META record with entries for both nisaba and metis, and gates rendering on isWebNavigableDomainId(domainId) — if a path is not a navigable domain it calls notFound().
  • apps/oshun/web/src/navigation/routes.ts:52-53 defines WEB_DOMAIN_IDS = ['tara', 'veritas', 'nyx', 'arete', 'nisaba'] and WEB_NAVIGABLE_DOMAIN_IDS = [...WEB_DOMAIN_IDS, 'metis']. So nisaba is in the static set and metis is added to the navigable set — both pass the isWebNavigableDomainId gate.
  • The shared @oshun/navigation route map at libs/oshun/navigation/src/routes.ts (lines ~98–130) maps all six domains to webPath: '/domains/<domain>' with deepLinkBase: 'oshun://<domain>', including explicit nisaba (/domains/nisaba, oshun://nisaba) and metis (/domains/metis, oshun://metis) entries. Its tests assert getDomainRouteDefinition('metis').webPath === '/domains/metis'.

The practical truth: /domains/nisaba and /domains/metis are reachable web routes — served by the [domainId] catch-all rather than by static directories, but reachable all the same. The dual-namespace pattern applies to every domain, not four-plus-exceptions. The two namespaces are intentional audience splits, not deprecation candidates for each other; see Customer-Facing Domains and the companion Product Surfaces for the per-domain room and deep-tool inventory.

Why a catch-all instead of two more static dirs? The static directories (arete, nyx, tara, veritas) host heavier bespoke client workspaces; Nisaba and Metis route through the data-driven [domainId] page, which reads from DOMAIN_META and the navigation route map. That keeps newly navigable domains additive — a domain becomes reachable the moment it is added to WEB_NAVIGABLE_DOMAIN_IDS and DOMAIN_META, without scaffolding a new page tree. The consumer hubs themselves are also real and rich: apps/oshun/web/src/app/nisaba/ carries subdirs like compare, daily, graph, lexicon, manuscript, notebook, scholar, and apps/oshun/web/src/app/metis/ carries assessment, byom, courses, ingest, lesson, session, tutor, upload.

One honest caveat on the live shell count#

The web experimentation layer carries a shellDomainCount (and a shellNavigationDomainCount) in its FlagEvaluationContext (apps/oshun/web/src/experimentation/feature-flags.ts). Per the 2026-06-23 triage, the live shell's shellDomainCount can drop from five to four post-hydration when Nisaba evaluates as disconnected and Metis as planned. So while the /domains/* routes for all six domains exist and resolve, the runtime-active domain count surfaced to the shell can be smaller than the route map suggests at a given moment. The routing capability is real; the live activation is flag- and connection-gated. This is the architecture being honest about the difference between "reachable route" and "active domain," not a contradiction.

Reading Order from Here#

This page is deliberately the shallow map. The next layer of detail lives in: