This page describes the load-bearing layer beneath every Oshun V1 domain: the canonical contracts, the persistence-alignment harness, the event and queue substrate, the inbound/outbound integration plumbing, the public-API authorization model, and the security controls that gate launch. It serves platform engineers, integrators, operators, and security reviewers — anyone who needs to know how a request travels from the shell to a domain service and back, what guarantees hold along the way, and which pieces are real versus spec-only. It sits "below" the customer-facing domain pages (Tara, Arete, Veritas, Nyx, Nisaba, Metis) and underneath the channel surfaces (Channel Abstraction); nearly all of it is implemented code with tests, not aspiration. Where a piece is contract-level or provider-gated, this page says so explicitly.
Candor note up front. This layer is overwhelmingly implemented: the contracts, the persistence drift/index/migration/tombstone test quartet, the OAuth 2.1/PKCE state machine, the role/scope model, the partial-failure envelope contract, the durable queue + dead-letter + SLA monitor, the topic-registry + outbound delivery + webhook-simulator event bus, the inbound connectors, the data-residency enforcer, the idempotency middleware, and the append-only audit hash chain all exist as real code. Two things are honestly narrower than they sound: the OAuth module is a contracts/state-machine implementation (token types, PKCE validation, refresh, revocation cascade) — not a deployed live authorization server — and the event bus is not built on native Redis Streams (see the correction below). Both are called out where they appear.
Where the foundations live#
The platform-foundations package is the single named home for the cross-cutting
subsystems. libs/oshun/platform-foundations/src/index.ts re-exports exactly
nine subsystems, and the documentation should treat this package
(@oshun/platform-foundations) as their canonical address rather than
scattering them:
| Subsystem (export) | Path | Responsibility |
|---|---|---|
service-discovery |
src/service-discovery/service-discovery.ts |
Domain → service routing, health/readiness, deprecation |
public-api |
src/public-api/oauth.ts |
§27 OAuth 2.1/PKCE, scoped tokens, quotas, audit |
shared-contracts |
src/shared-contracts/ |
Cross-subsystem canonical shapes |
role-model |
src/role-model/role-model.ts |
§28 canonical roles, scopes, function-level authorization |
step-up |
src/step-up/step-up.ts |
Step-up authentication for sensitive actions |
secrets |
src/secrets/secrets.ts |
Standardized secret loading, scoping, rotation |
configs |
src/configs/configs.ts |
Validated feature flags, policy bundles, env configs |
rollback |
src/rollback/rollback.ts |
Safe rollback plans for shell/admin/grounding/persona/generation |
abuse-controls |
src/abuse-controls/abuse-controls.ts |
Rate limits, payload caps, query-cost ceilings |
Related building blocks live in sibling shared libraries — the auth primitives
(libs/shared/auth-primitives), the HTTP client middleware
(libs/shared/http-client), the event bus (libs/shared/event-bus), the queue
substrate (libs/shared/queue), the inbound connectors
(libs/shared/inbound-integrations), data residency
(libs/shared/data-residency), and the audit platform
(libs/shared/audit-platform). The auth client package used by services is
@oshun/auth-client (libs/oshun/auth).
The canonical domain registry#
Every top-level domain is described once, in code, by the domain registry at
libs/oshun/domain-registry/src/registry.ts. OSHUN_DOMAIN_IDS enumerates the
six customer domains —
['tara', 'veritas', 'nyx', 'arete', 'nisaba', 'metis']
— and OSHUN_SHELL_PRIMARY_DOMAIN is 'tara', the domain the shell opens to.
DOMAIN_REGISTRY maps each id to a DomainMetadata record that carries far
more than a label: a DomainAuthPolicy (required, a sessionKind of
customer | learner | reader | operator, scopes, and stepUpActions), an
'analytics-id', a 'notification-channel', an 'assistant-context-key', a
'deep-link-prefix', a 'bff-base-path', a DomainLaunchContract with typed
permissions (notifications | audio | location | calendar | camera | storage),
a DomainOfflineFallbackCard, a DomainShellNarrative, and an
'admin-taxonomy' (ownerSubsystem, primaryQueues, auditCategory,
contentClasses). The BFF base paths are stable and uniform:
| 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 |
Because a domain's route typing, auth/domain preferences, analytics enums, notification preferences, assistant context key, deep-link prefix, and admin taxonomy all originate from one record, the shell, the BFF, analytics, and the admin console cannot drift out of agreement about what a domain is. Adding a seventh domain means adding one entry, not editing eight subsystems.
Shared contracts as the spine#
V1's request and response shapes are canonical Zod contracts. They cover the breadth the source enumerates — rituals, practices, concepts, passages, claims, sources, notebooks, collections, continuity state, personas, voice profiles, avatar packs, evidence packs, review packages, provenance bundles, consent records, memory scopes, workflow templates, model cards, model versions, support cases, incidents, and policy bundles — and they are the enforcement point for object-property authorization, excessive-data-exposure, and mass-assignment defenses (a request shape that doesn't model a field cannot smuggle it through).
Common contracts shared across domains live under libs/contracts/src/common/.
That directory contains 286 files today — 161 implementation .ts modules
plus their colocated .spec.ts/.test.ts siblings. (An earlier doc figure of
"≈265 files" is stale and low; the exact total drifts up as contracts gain
tests, so treat the implementation-module count as the stable figure.)
The contracts index (libs/contracts/src/index.ts) does more namespace
re-exporting than the three commonly cited (NisabaContracts, MetisContracts,
VeritasContracts). It also namespaces the cross-version and living-scene
contract sets, using explicit aliasing where names would otherwise collide:
| Namespace export | Source |
|---|---|
NisabaContracts |
./nisaba |
MetisContracts |
./metis |
VeritasContracts |
./veritas |
IrisContracts |
./iris/index |
AgentContracts |
./agent/index |
OyaContracts |
./oya/index |
V3Contracts |
./v3/index |
V6Contracts |
./v6/index |
V9Contracts |
./v9/index |
LivingSceneContracts |
./living-scene/index |
The living-scene set is aliased to avoid clashes (for example
LivingSceneScoreSchema and deepParseLivingSceneScore), so a consumer can
import scene contracts alongside domain contracts without symbol collisions. See
Scene Score Schema for what those shapes model.
The partial-failure envelope contract#
When an operation fans out across domains (say, a bulk import touching Veritas
claims and Nisaba passages), some items can succeed while others fail. V1
represents that outcome as a first-class Zod contract, not an ad-hoc shape.
It lives at libs/contracts/src/common/partial-failure-envelope.ts:
// PartialFailureEnvelopeSchema (shape)
{
results: unknown[],
errors: PartialFailureError[], // { domain, stage?, message }
partial: boolean,
}
The module exports PartialFailureEnvelopeSchema, the per-error
PartialFailureErrorSchema (domain and message required, stage optional),
a generic factory
createPartialFailureEnvelopeSchema(resultSchema, errorSchema?) for typing the
results/errors arrays per call site, a constructor
buildPartialFailureEnvelope(...), and the type guard
isPartialFailureEnvelope. The contract enforces two cross-field invariants via
superRefine, so an envelope cannot lie about its own state:
partial === truerequires at least one error entry, andpartial === falserequires the errors array to be empty.
buildPartialFailureEnvelope goes further and infers partial from whether any
errors are present, throwing if a caller passes a partial flag that disagrees
with the errors it supplied. The result is a uniform "here's what worked, here's
what didn't, and yes this was a partial outcome" envelope that every fan-out
caller can produce and every consumer can validate identically.
Contract-to-persistence alignment#
Canonical Zod schemas are kept honest against the Prisma data models by a real
alignment harness in libs/oshun/persistence/src. The centerpiece is the
introspection + rendering pair — zod-prisma-introspection.ts reads the Zod
contracts, and prisma-renderer.ts projects them — backed by the
drift/index/migration/tombstone test quartet the source promises:
| Test | What it guards |
|---|---|
contract-persistence-registry.test.ts |
Every persisted contract is registered and mapped |
index-requirements.test.ts |
Required query indexes exist for declared access patterns |
migration-plan.test.ts |
Schema changes produce a coherent, ordered migration plan |
tombstone-semantics.test.ts |
Soft-delete / tombstone behavior is correct and consistent |
Privacy deletion is implemented in the same library: dsar-deletion-cascade.ts
and dsar-erasure-runtime.ts (each with unit and integration tests) carry the
DSAR (data-subject-access-request) cascade through the persistence layer, so an
erasure request actually propagates rather than merely flagging a row. Durable
stores for memory, snapshots, and the admin audit-events stream
(durable-memory-store.ts, durable-snapshot-store.ts,
durable-admin-audit-events-store.ts) provide the replayable critical histories
the source calls for — review state, approval state, memory, generation/research
jobs, persona release state, provenance bundles, audit events, and
review/incident timelines.
Inter-service contracts: OpenAPI and proto#
External and internal service contracts are published, not implicit.
libs/openapi/src/specs/ holds 15 real spec directories plus the aggregate
main.yaml and v3.yaml:
arete bellona calliope concordia hathor isis lilith
metis nisaba nyx oshun-bff sophia tara veritas yemaya
For typed cross-service RPC, gRPC/proto is real and broad. libs/proto carries
a buf.work.yaml, a generated/ tree, and roughly two dozen proto domain
directories under src/ — agent, ai, asset, auth, bridge,
collaboration, common, concordia, generation3d, health, hathor,
isis, loadbalancing, oshun, oya, pipeline, procedural, project,
reflection, rendering, shared, sophia, splatting, and user. (The
older doc that named only Psyche/Isis/Sophia understated the proto footprint
substantially.)
The public-API platform: OAuth 2.1 / PKCE#
libs/oshun/platform-foundations/src/public-api/oauth.ts is the §27 public-API
authorization layer. To be precise about scope: this is a contracts and
state-machine implementation — token shapes, PKCE validation, refresh
rotation, revocation cascade, and quota evaluation — that the BFF and domain
services compose. It is not, by itself, a deployed live authorization server;
treat the deployment of an authorization-server endpoint as the integration step
around this verified core.
The module supports the grant types authorization-code, refresh-token, and
client-credentials, with two token types:
TOKEN_TYPES = ['access', 'refresh']. An OauthClient declares its
clientKind (first-party | third-party), redirectUris, allowedScopes,
whether it requiresPkce, separate access and refresh token lifetimes, a
perKeyRateLimitPerMinute, and an optional tenantId for tenant-scoped
clients.
PKCE is enforced structurally. The code challenge must match
PKCE_CODE_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43,128}$/ and the verifier its
own pattern, and only the S256 challenge method is accepted —
verifyPkceChallenge recomputes the SHA-256 of the verifier (via
@noble/hashes) and compares. validateAuthorizationCodeRequest returns a
typed list of AuthorizationCodeErrors rather than throwing, covering the real
failure modes:
| Error code | Meaning |
|---|---|
client-mismatch |
Request clientId ≠ registered client |
invalid-redirect-uri |
Redirect URI not in the client's allow-list |
scope-not-allowed |
Requested scope outside allowedScopes |
pkce-required |
Client requires PKCE but none supplied |
invalid-code-challenge |
Challenge fails the pattern |
unsupported-challenge-method |
Method ≠ S256 |
code-expired / code-consumed |
Authorization code reused or stale |
pkce-verification-failed |
Verifier doesn't hash to the challenge |
Refresh and revocation are modeled honestly as a state machine.
redeemRefreshToken(...) rotates tokens: it rejects a non-refresh token
(wrong-token-type), an already-revoked token (revoked-refresh), or an
expired one (expired-refresh); on success it returns a fresh access token, a
fresh refresh token, and the now-revoked prior refresh — and it stamps the new
access token's mintingRefreshTokenId so lineage is traceable. That lineage is
what revocationCascade(...) walks: revoking a refresh token id revokes that
token and every access token minted from it. Quotas are evaluated by
evaluatePublicApiQuota(...) against per-key windows, and privileged API
activity is captured via buildPublicApiAuditEvent(...). Tokens carry scopes,
tenantId, and subjectUserId, so authorization stays tenant-aware across the
BFF and domain services.
The auth-primitives library (libs/shared/auth-primitives/src) is richer than
"JWT and session primitives": it ships api-key.ts, jwt.ts, session.ts,
oauth-client.ts, oauth-revoke.ts, token-refresh.ts, token-audit.ts,
totp.ts (TOTP for step-up), platform-roles.ts, tenant-isolation.ts, and
password.ts, each with colocated specs.
The canonical role and scope model#
Access control is anchored by CANONICAL_ROLES in
libs/oshun/platform-foundations/src/role-model/role-model.ts — ten roles,
not the looser ~eight the prose previously implied. The exact set is:
customer, creator, support-agent, reviewer, moderator,
privacy-operator, model-operator, persona-operator,
tenant-admin, admin-leadership
Two of these deserve surfacing because the older prose hid them: creator is a
distinct canonical role (it adds the creator.studio scope on top of
customer.self/customer.shell), and tenant-admin is distinct from
admin-leadership (a tenant-admin gets tenant.console and tenant-scoped
audit, while only admin-leadership gets the cross-tenant and break-glass
scopes). The prose name support should read support-agent to match the code.
Scopes are explicit (SCOPE_KEYS) and mapped per role; scopesFor(role)
returns the scope set, and filterFieldsByRole(...) shapes responses by role.
The scope-to-role mapping is least-privilege by construction:
| Role | Granted scopes |
|---|---|
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 |
Function-level authorization runs through authorizeAction(...), which takes
the subject's roles, the requiredScope, and the subject/target tenant ids and
returns a typed AuthorizationDecision whose denial reason is one of
role-not-permitted, tenant-isolation, or object-not-owned. Crucially,
cross-tenant authorization (a non-null target tenant that differs from the
subject's) requires admin-leadership — so a tenant-admin cannot reach across
into another institution's data. Step-up authentication
(platform-foundations/src/step-up/step-up.ts, backed by totp.ts) gates the
sensitive actions a domain declares in its stepUpActions.
The event bus — and a correction#
Cross-domain integration runs over the event bus at
libs/shared/event-bus/src/event-bus.ts:
export class EventBus implements IEventBus, instantiated by the factory
createEventBus(config: EventBusConfig). It uses
import { Redis } from 'ioredis' with separate publisher and subscriber
connections, and it ships the topic registry, outbound delivery, and
webhook-simulator pieces the source promises (the default registry is
DEFAULT_EVENT_TOPIC_REGISTRY in topic-registry.ts, with schema-versioned
topics and payload validation via EventPayloadValidationIssue).
Accuracy correction. V1/DEPENDENCIES.md historically labeled this an
"Event Bus (Redis Streams)" — the one outright-wrong technical claim in this
area, since corrected in the registry. The implementation does not use
native Redis Streams — there is no XADD/XREAD/XREADGROUP/XGROUP
anywhere. Per the module's own header, it is built from ordinary Redis
primitives:
publish()serializes the envelope, stores it in a TTL-bounded key (defaultDEFAULT_EVENT_TTL = 86400seconds — the replay source), and fans it out to subscribers over Redis pub/sub. A separate HASH tracks which subscriptions/consumer-groups have already processed an event so crash-restarts don't redeliver acked work.ack()marks(eventId, subscriptionId)or(eventId, group)processed; unacked work is redelivered on the nextreplayUnackedsweep.nack(delay)and delayedpublish({ delay })reschedule through a durable sorted set namedscheduled; a scheduler loop (DEFAULT_SCHEDULER_INTERVAL = 250ms) pulls due entries —setTimeoutis never the only copy, so it's crash-safe.- Consumer groups are emulated: when a subscription declares a
group, members race viaSET NXon a per-(eventId, group)claim key, and only the winner runs the handler; without a group, every matching subscription runs (classic broadcast). - Dead-letter entries live in a list (for ordered pagination) and a
hash (for O(1) id lookup);
removeDeadLetterusesLREMon the serialized entry for atomic removal.
So the BFF→…→event-bus→audit composition the architecture diagrams describe is
real and matches the building blocks present (idempotency → tenant → residency →
Zod → adapter → event-bus → audit). Only the "Redis Streams" substrate label was
wrong; the accurate description is "Redis pub/sub with TTL-keyed replay, a
scheduled sorted set, and SET NX consumer-group claims." (We have not traced
a single production BFF route proving every middleware fires in exactly that
order in deployed wiring — the components exist; the end-to-end order is a
composition claim.) Outbound webhook delivery rides on top with signing,
retry/backoff, dead-letter routing, replay, and delivery audit.
The durable queue substrate#
Cross-domain background jobs (editorial, asset, agentic AI, notification,
billing, integration) run on the queue substrate at libs/shared/queue/src,
which matches the source's "durable queues, replay, deduplication, priority
classes, observability" claim with real modules:
| Module | Role |
|---|---|
durable-queue.ts |
Persistent, replayable job queue with priority classes |
dead-letter-queue.ts |
DLQ for jobs that exhaust retries |
sla-monitor.ts |
SLA tracking / observability over queued work |
memory-queue.ts |
In-memory queue (tests / single-process) |
worker.ts |
Worker loop that drains and acks jobs |
The durable queue, DLQ, and SLA monitor each ship colocated specs.
Inbound integration connectors#
Inbound integration plumbing lives in libs/shared/inbound-integrations/src,
whose index.ts exports 15 connector modules — substantially more than the
nine-row table older docs showed. These are sizable, real implementations (for
example oneroster.ts is ~1081 LOC, lms.ts ~1425 LOC, and identity.ts ~1048
LOC):
| Connector module | Covers |
|---|---|
types |
Shared connector descriptors and health types |
lms |
LMS integration (LTI 1.3 / LTI Advantage surface) |
lti-verification |
LTI launch/JWT verification |
scorm-rte |
SCORM run-time environment |
scorm-2004-rte |
SCORM 2004 run-time environment (distinct from scorm-rte) |
oneroster |
OneRoster rostering |
identity |
SAML 2.0 / OIDC / SCIM 2.0 identity |
calendar |
Calendar integration (Google / Apple / Outlook) |
calendar-google-transport |
Google-specific calendar transport |
payment |
Payment connector |
telemetry |
Telemetry sinks (xAPI / cmi5 / Caliper) |
byom |
Bring-your-own-model ingestion |
byom-model |
BYOM model descriptor (distinct from byom) |
notification |
Partner notification sinks |
health |
Connector health reporting |
Note the modules older tables omitted: the two SCORM RTEs (scorm-rte and
scorm-2004-rte), lti-verification, the separate byom-model, and
calendar-google-transport. Each connector carries version pinning and health
reporting, and the framework supports declarative descriptors, sandbox tenants,
certification flows, OpenAPI specs, code samples, and operator-managed
enable/disable per tenant. Metis is the primary consumer of
LMS/OneRoster/identity (see Metis).
HTTP-client middleware: idempotency, SSRF, breakers#
The outbound HTTP client (libs/shared/http-client/src) carries the request
hygiene the architecture leans on:
- Idempotency —
idempotency.tsexportsDEFAULT_IDEMPOTENCY_HEADER = 'Idempotency-Key',DEFAULT_IDEMPOTENCY_TTL_MS = 24 * 60 * 60 * 1000(24h),DEFAULT_IDEMPOTENCY_MAX_ENTRIES = 1000, anIdempotencyMiddlewareclass, andcreateIdempotencyMiddleware(...). It defaults to guarding mutating methods (POST/PUT/PATCH/DELETE), fingerprints the request, and throws typed errors —IdempotencyKeyRequiredError,IdempotencyRequestMismatchError,IdempotencyWaitAbortedError,IdempotencyFingerprintError— so a replayed key with a different body is rejected rather than silently re-run. - SSRF guard —
ssrf-guard.tsblocks unsafe-upstream consumption for URL ingestion, fetch tools, webhooks, BYOM ingestion, and retrieval connectors. - Resilience —
circuit-breaker.ts,retry.ts,timeout.ts, plustenant-context.ts(tenant-aware request scoping) andtracing.ts.
Data residency#
Residency enforcement is real, in libs/shared/data-residency/src:
enforcer.ts decides whether an operation may touch data in a given zone,
home-zone.ts models a tenant's home region, traffic-shaping.ts implements
residency-aware traffic shaping, dsr-routing.ts routes data-subject requests,
and an injected ResidencyAuditPublisher seam (exercised by
audit-platform-bridge.spec.ts) ties residency decisions into the audit chain.
Together these back the "multi-tenant routing, residency-aware traffic shaping,
tenant-aware caching/flags/experiments" guarantees the source lists.
The audit platform — an append-only hash chain#
Audit is not a single sentence; it is a large substrate
(libs/shared/audit-platform/src, ~40 implementation modules). The integrity
backbone is hash-chain.ts, which implements an append-only, tamper-evident
hash chain: hashEvent(...) hashes a CanonicalPlatformAuditEvent,
chainHashStep(previousChainHash, eventHash) links each entry to its
predecessor, and HashChainedAuditEventStore /
createHashChainedAuditEventStore(...) persist the chain. Verification
(AuditChainVerificationResult, AuditChainMismatch) detects breaks and raises
AuditChainTamperDetectedError, and canonicalSerialize(...) guarantees stable
hashing across runs. Around that core sit compliance and provenance modules,
including schema-versioning, retention, escalation-rules,
support-escalation, compliance-attestation, watermark-verification,
synthetic-media-labeling, voice-likeness-consent, provenance-attachment,
and provenance-badges. This is what gives the privileged-action workflows
(support, review, moderation, privacy, model, persona, copilot override,
incident) their immutable, per-flow audit assertions.
Security as a launch gate#
V1 is a multi-tenant, multi-role product handling consented memory, synthetic media, educational records, and institutional rosters, so security is treated as a launch gate, not post-launch hardening. The role/scope model, step-up auth, audit hash chain, idempotency, SSRF guard, residency enforcement, and abuse controls above are the implemented core. The broader program the source enumerates — and the honest status of each control — is:
| Control area | Status |
|---|---|
| Canonical role model, least-privilege scopes, step-up, tenant/env separation | Implemented (role-model, step-up, tenant-isolation) |
| Immutable audit + per-flow audit assertions | Implemented (audit-platform hash chain) |
| Standardized secrets handling, rotation, scoping | Implemented (platform-foundations/secrets) |
| Validated configs, policy bundles, experiment guardrails, rollback plans | Implemented (configs, rollback) |
| Object-property auth, excessive-data-exposure, mass-assignment defense | Implemented via canonical Zod shapes + filterFieldsByRole |
| Resource-consumption / abuse controls (rate, payload, pagination, query-cost) | Implemented (abuse-controls) |
| SSRF / egress-policy / unsafe-upstream enforcement | Implemented (ssrf-guard) |
| Permission/authorization regression suites, tenant-isolation/leakage suites | Implemented (function-level authorizeAction + isolation specs) |
| Authenticated DAST against staging web/admin/APIs | Process/CI gate — exercised against deployed staging |
| SAST + CodeQL, dependency-vuln + secret scanning | CI/process gate |
| SBOM + provenance verification + release signing | Process gate, "where supported" |
| Fuzz / malicious-input suites (uploads, markdown, search, tool/webhook surfaces) | Process gate |
| API inventory / shadow-endpoint discovery per release | Process gate |
| Pre-GA pen-test + red-team (auth, RBAC, privacy, Metis, grounded gen, escalation) | External engagement, scoped + signed off |
The bottom rows are deliberately marked as process/CI/external gates rather than "shipped code": DAST, pen-testing, SBOM signing, and red-team exercises are launch activities run against deployed environments, and the docs keep that candor rather than claiming a green checkmark for work that happens at release time.
Why it works this way#
The recurring pattern across these foundations is one canonical definition, many consumers: a single domain registry, a single contracts spine, a single role/scope model, a single audit chain, a single idempotency policy. Drift is the enemy of a multi-tenant, multi-domain platform — eight subsystems each holding their own copy of "what a domain is" or "what a customer may do" is how cross-account leakage and authorization gaps creep in. By making the source of truth executable (and testing the alignment with the persistence quartet and the authorization suites), the platform turns "did we keep these in sync?" from a review question into a CI failure.
Related#
- Subsystem Glossary — names and ownership for every subsystem referenced here
- Privacy, Consent, Data Portability, and User Controls — DSAR cascade and consent records in practice
- Channel Abstraction, Routing, and Boundaries — how channels sit above the BFF
- Tenant, Institution, and Operator Toolkit — operator-facing connector enable/disable and tenant scoping
- Review, Compliance, and Trust & Safety — the privileged workflows the audit chain covers
- Analytics, Observability, Testing, and Security — the testing and security program around these foundations
- Metis — Education and Tutoring — primary consumer of the LMS/OneRoster/identity connectors
- ../features.md — feature hub