Oshun Platform · Features

Admin Products — Web and Mobile

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

8sections17 minread4tables

On this page

The Admin products are the operator-facing control surfaces of Oshun V1: the Oshun Admin web console that platform operators use to run review, trust & safety, support, privacy, model ops, incidents, and readiness; and Oshun Admin mobile, an Expo app that puts urgent triage and step-up-protected approvals in an on-call operator's pocket. This page serves the operators who live in these surfaces, the engineers wiring the BFF route handlers, and reviewers who need to know which claims are backed by shipped code versus partially-covered UI. It sits beside the strictly-isolated Tenant, Institution, and Operator Toolkit: the operator-only Admin products described here are the platform-wide counterpart to the per-tenant console, and both draw on a shared domain library, libs/oshun/tenant-console.

Scope note. "Admin Products" is the operator console. The capabilities that a tenant admin sees — SSO/SCIM config, roster sync, per-tenant auth policy, custom roles — are detailed on Tenant, Institution, and Operator Toolkit. Both surfaces are thin Next.js rendering layers over the same nine @oshun/tenant-console modules; this page focuses on the platform-operator web and mobile apps and enumerates their concrete route surface. Backlog tracking for the console substrate is §20 in ../TODOS.md; the apps themselves live under apps/oshun/admin, apps/oshun/admin-mobile, and apps/oshun/tenant-admin.


Where this sits, and what is real#

The Admin web app (apps/oshun/admin) is a substantial Next.js application: 274 files under src/components/ and a deep server route surface under src/app/api/admin/. It is not a wireframe. Every operator workflow described below is backed by a real BFF route handler that, in turn, calls real domain logic — much of it the pure-function substrate in @oshun/tenant-console.

The realism caveat is the same one that governs the whole admin-tenant area and is stated honestly here: the identity primitives (processSsoLogin, applyScimSync, evaluateAuthChallenge) are deterministic pure functions, not live SAML/OIDC runtimes. There is no embedded SAML response parser or OIDC token validator in the library. The OIDC live-login path is wired to fail closed with a 503 when an SsoConnection lacks oidcClientId / oidcJwksUrl, exactly as the comment in identity/sso.ts documents, "so a config-only connection never pretends to be loginable." This is a deliberate fail-loud seam, not a stub: the function refuses to fabricate a logged-in session it cannot actually authenticate. The one area the completeness audit flags as only partially covered end-to-end is the tenant-admin SSO config UI (metadata-XML / OIDC-discovery parsing, the full claim-transform matrix editor, the auth-policy panel, and sandbox-probe verdicts). Nothing in this area reads as fabricated.


Oshun Admin Web#

Oshun Admin web V1 includes a single operator shell with auth, RBAC, a dashboard, a unified inbox, and navigation across trust/safety, review, research-integrity, support, privacy, policy, model, persona, rights, incident, and readiness areas. Across that shell, it provides saved views, filters, a universal admin search, cross-workspace links, collaboration comments and annotations, keyboard-first triage, a notification center, assignment signals, and exportable investigation bundles.

The operator workspaces#

The console is organized into named workspaces, each with list and detail views, its own queue semantics, and an audit linkage:

  • Review. Package lists and details, stage graphs, status history, approve / reject / request-changes actions, rationale capture, delegation, escalation, SLA timers, review templates, blockers, immutable history, and audit linkage.
  • Trust and safety. Queues for moderation, user reports, crisis escalation, cloned-voice and avatar-deception review, image moderation, repeat-offender tracking, appeals, rule-hit explanations, and metrics.
  • Content & curriculum. Content calendar, publishing states, content QA, curriculum and teacher publishing, research integrity, and citation validation.
  • Support, billing & privacy. Support cases, subscriptions, entitlements, refunds, privacy requests, DSAR exports, deletion, retention exceptions, legal holds, policy attestations, and compliance reviews.
  • Persona, model & rights. Persona registry, voice-profile review, avatar-pack review, model registry, workflow templates, rights/provenance inspection, watermark verification, incidents, a policy/config center, and executive and readiness dashboards.
  • Editorial. Editorial calendar, content scheduling, embargo, takedown, hotfix, and emergency-correction surfaces with cross-domain visibility and assignment.
  • Agentic AI operations. Dashboards bound to the Agentic AI Studio (see Agent Registry, Job Orchestration, and Multi-Agent Plans): cross-tenant run inventory, plan/DAG inspection, evidence-trail review, cost ledger, kill-switch activation, gold-set promotion, champion-challenger rollout, and rollback.
  • Audit log explorer. Filters, saved investigations, exportable bundles, cross-event correlation, and tenant-scoped views (mechanics below).
  • Bulk-operation console. Users, rosters, content, taxonomy, persona assignments, and entitlements with dry-run, validation, and staged commit.
  • Integrations registry. API keys, webhooks, LMS/calendar/identity/payment connectors, OpenAPI/SDK docs, sandbox tenants, and webhook simulators.
  • Communications. Notification, email, SMS, and in-app message template authoring with localized variants, variable validation, accessibility lint, A/B testing, and approval workflow; help center and changelog authoring under the same lifecycle, review, localization, and versioning machinery as content domains; and public status-page authoring, banner targeting, incident communications, and a postmortem catalog.

Admin copilots, with a hard line between suggestion and decision#

Each operator area carries a copilot — for review, moderation, research integrity, model ops, compliance, and support — with an explicit distinction between suggestions, evidence, and human decisions. High-risk decisions require mandatory human approval; the console captures feedback, logs overrides, ships audit tests, and is built to prevent bypass. This is visible in the route surface: review decisions go through api/admin/review/[reviewId]/confirm-high-risk-approval and a paired .../cancel-high-risk-approval. Every copilot area exposes a .../copilot-feedback endpoint so a human's accept/reject of a suggestion is itself recorded.

One inbox, safe bulk triage, the full policy surface#

A single canonical inbox-and-queue model spans review, moderation, support, privacy, incident, model, persona, policy, and research integrity. Safe bulk triage and action workflows enforce rationale, selection limits, previews, and audit trails (the bulk inbox action is api/admin/inbox/bulk-action). The policy surface is correspondingly broad: policy packs, disclosure state, tone/style calibration, prompt policy, retrieval policy, grounding policy, moderation thresholds, appeals, voice/avatar policy, experiment guardrails, model lineage, provider health, sampled output review, incident blast-radius, customer impact, runbook launch, postmortems, release scorecards, and go/no-go decisions.

Designed for long shifts#

The console favors a dense-data redesign, calmer operator surfaces, evidence comparison, diff inspection, severity and escalation visuals, reduced modal overuse, and extended-session ergonomics. Coverage spans Playwright, accessibility, visual regression, audit, copilot, admin-queue, policy, incident, export, rights, privacy, and research-integrity tests.


The Admin web server route surface#

The architecture docs describe the admin app but stop short of enumerating its server routes; in practice the BFF surface under apps/oshun/admin/src/app/api/admin/ is the most concrete evidence that the console is wired through to the domain libraries rather than mocked. A representative slice (the full tree is deeper, with per-resource [id] and action sub-routes):

Area Representative routes
Session signin, signout
Discovery search
Inbox & notifications inbox/bulk-action, notifications, notifications/[notificationId]/{seen,dismiss}, notifications/mark-all-seen
Review review/[reviewId]/{decision,delegate,escalate,confirm-high-risk-approval,cancel-high-risk-approval,copilot-feedback}
Moderation moderation/items/[contentItemId], .../copilot-feedback
Bulk operations bulk-operations/[operationId]/transition, bulk-exports/[opId]/verify
Integrations integrations/api-keys, integrations/api-keys/revoke, integrations/webhooks/simulator, integrations/connectors/upgrade, integrations/snapshot
Developer portal developer-portal/articles, developer-portal/certification/checks, developer-portal/sandbox-policies
Agentic operations agentic-operations/kill-switches, .../gold-sets/promote, .../champion-challengers/rollout, agentic-operations/snapshot
Communications communications/{banners,help-articles,incidents/update,status-components,snapshot}
Editorial editorial/release-streams, .../[streamId]
Audit audit-log/events, audit-log/investigations/[investigationId]/export
Privacy privacy/{requests,deletion-jobs,consent-records,disclosure-entries,memory-scopes,retention-exceptions,audit-exports/generate}
Models & providers models/[modelId]/{governance/promote,governance/rollback,providers/failover/trigger,review/decision,spot-check/...}
Readiness & rehearsals readiness/reports/[reportId]/releases/[releaseId]/decision, rehearsals/[rehearsalId]/{start,complete,rollback}

These routes map directly onto the @oshun/tenant-console exports — e.g., bulk-exports/[opId]/verify calls verifyManifest, integrations/api-keys operates on ApiKey records, communications/incidents/update appends to an IncidentRecord.publicUpdates thread, and audit-log/investigations/.../export materializes an AuditExportBundle with chain-of-custody metadata.


The shared substrate: @oshun/tenant-console#

Both the operator console and the tenant console import the same library. libs/oshun/tenant-console/src/index.ts re-exports nine modules: tenant-model, identity, roles, audit-explorer, bulk-ops, integrations, notifications, help-center, and status-page. The apps are thin Next.js rendering layers over these. apps/oshun/tenant-admin/src/app/identity/page.tsx imports applyScimSync, canFederateAuth, computeRosterDiff, evaluateAuthChallenge, and processSsoLogin directly from @oshun/tenant-console, so the app genuinely consumes the lib rather than reimplementing identity logic in the UI. (ARCHITECTURE.md correctly names the app at apps/oshun/tenant-admin but understates the library's role: essentially all domain logic lives in the library; the Next app is the rendering layer.)

The subsections below surface the concrete primitives that the operator console drives — useful both as a reference for the route handlers and as evidence that the logic is domain-specific, not generic CRUD.

Identity — SSO, SCIM, OneRoster, federation, auth policy#

identity/sso.ts models SSO_PROTOCOLS = ['saml2','oidc']. A claim mapping (SsoClaimMapping) maps an IdP externalClaim to one of a fixed set of internal attributes — email, displayName, familyName, givenName, groups, tenant-role, preferred-locale — under one of four transforms: identity, lowercase, csv-split, first-only. processSsoLogin handles both idp-initiated and sp-initiated flows, validates the assertion signature, enforces session/refresh lifetime sanity, applies the transforms, and performs just-in-time provisioning when jitProvisioning is on and the email claim is present. As noted above, OIDC live login fails closed (503) when the runtime fields are absent.

identity/scim.ts defines SCIM_OPERATIONS = ['create','replace','patch','delete'] and a conflict policy ScimConflictResolution ∈ {reject, merge, idp-wins, local-wins}. applyScimSync returns a verdict — applied, rejected-conflict, merged, or noop — so the operator sees exactly how a provisioning event resolved. The module also carries OneRosterClassResource and OneRosterEnrollmentResource (each with status ∈ {active, tobedeleted, inactive}) and computeRosterDiff, which produces an add/update/remove diff plus conflict reports (duplicate externalIds, enrollments referencing unknown classes or users) for OneRoster-grade Metis rostering.

identity/federation-and-auth-policy.ts provides cross-tenant federation via canFederateAuth, supporting both one-way and mutual TenantFederationEdges. The per-tenant TenantAuthPolicy carries mfaMethodsAllowed ∈ {totp, webauthn, sms, push}, an ipAllowlist (CIDR-style), a device-posture requirement, and inactivity bounds. evaluateAuthChallenge returns allow, step-up-required (reason mfa / device-posture / inactivity / trigger), or deny (reason ip-blocked). The step-up triggers are fixed: AUTH_STEP_UP_TRIGGERS = ['high-risk-action', 'export-sensitive-data', 'admin-override', 'incident-response', 'after-inactivity'].

Roles — diff, risk scoring, dry-run harness, recertification#

roles/roles.ts is the most algorithmically dense identity module. A tenant role template is diffed against the canonical baseline by diffTemplateAgainstCanonical, which classifies each capability's PermissionDiffEntry.delta as unchanged, granted-by-tenant, revoked-by-tenant, scope-narrowed, or scope-widened, and assigns a risk weight per the constants:

Delta Risk weight constant
granted-by-tenant RISK_WEIGHT_GRANT = 3
scope-widened RISK_WEIGHT_SCOPE_WIDEN = 4
revoked-by-tenant RISK_WEIGHT_REVOKE = 1
scope-narrowed RISK_WEIGHT_SCOPE_NARROW = 1

The summed riskScore drives requiresReviewerSignoff, which trips when the underlying canonical role isElevated or when the score reaches REVIEWER_SIGNOFF_THRESHOLD = 6. For example, two unapproved scope-widenings (4 + 4 = 8) force a reviewer, while a single grant (3) does not. The weighting is deliberately asymmetric: widening scope is the riskiest single act, and loosening permissions costs more than tightening them.

The module also ships the dry-run role test harness the feature spec mentions only generically. runDryRun takes a role's effective capabilities and a list of DryRunAction { capabilityId, scope, expected }, evaluates each against the allow/deny and scope-restriction logic, and returns DryRunResult { action, actual, matched } so an operator can assert allowed/denied actions and surface unexpected escalations before deploying a role. Alongside it is a real recertification state machine — ASSIGNMENT_STATES = ['pending','approved','active','expired','recertified','revoked'] with transitionAssignment enforcing legal transitions, an approver-required guard (the requester cannot approve their own assignment), and a past-expiry guard.

Audit explorer — a per-tenant hash chain#

audit-explorer/audit-explorer.ts does more than "tamper-evident storage." Each AuditLogRecord carries a contentDigest and a priorDigest — the digest of the previous record in the same tenant's log — forming a per-tenant hash chain. verifyAuditChain walks a segment and reports the first record where a priorDigest fails to match the prior contentDigest, so tampering or deletion is detectable, not merely discouraged. Records carry severity ∈ {info, warning, critical}. applyAuditFilter filters by actorId, actorRole, tenantId, action, severity, time window, targetObjectClass, policyBinding, and correlationId, and is viewer-scoped through ExplorerViewer { role: 'platform-operator' | 'tenant-admin', tenantId }: a tenant-admin viewer can never see another tenant's records. Export bundles (AuditExportBundle) carry chain-of-custody metadata and a record of redactions applied; applyRedactions supports remove / mask / pseudonymize modes for privacy-sensitive fields.

Bulk operations — dry-run, residency counting, per-kind export matrix#

bulk-ops/bulk-ops.ts defines BULK_RESOURCE_KINDS = ['users','rosters', 'classes','courses','content','sources','taxonomy','persona-assignments', 'entitlements'] and BULK_OP_KINDS = ['import','export','action']. dryRunImport produces a BulkDryRunReport that tracks per-row validation errors, conflict/add/update counts, a residencyViolationCount, and a readyToCommit flag that is true only when there are zero errors and zero residency violations. Staged commit (planStagedCommit) records committed, failed, and skipped row IDs plus a resume index; bulk actions are gated by gateBulkAction, which blocks on a missing rationale or an over-safetyLimit selection.

Export is its own module, bulk-ops/bulk-export.ts. The supported formats are BULK_EXPORT_FORMATS = ['json','csv','oneroster','xapi','caliper'] and the exportable resource kinds are BULK_EXPORT_RESOURCE_KINDS = ['content','rosters','users','audit','rights','metis']. Crucially, the docs flatten what is actually a per-resource-kind support matrix (BULK_EXPORT_FORMAT_SUPPORT) — not every format is valid for every kind:

Resource kind Allowed formats
content json, csv, oneroster
rosters json, csv, oneroster
users json, csv, oneroster
audit json, csv, xapi, caliper
rights json, csv
metis json, csv

executeBulkExport enforces this (format-not-supported), checks residency (residency-violation), chunks records (default 5000/file), and emits a signed BulkExportManifest with per-file SHA-256 hashes and a manifest hash. The hash function is dependency-injected (hashSha256) so the module is deterministically unit-testable and can be backed by node:crypto at runtime without coupling — a documented seam, not a fake. verifyManifest recomputes file and manifest hashes to confirm integrity.

Integrations — API keys, webhooks, connectors#

integrations/api-keys.ts stores only a key prefix and a tokenHash — the full key is never persisted, per the in-code comment. authenticateApiKey returns one of four explicit failure reasons: unknown-key, revoked, expired, ip-not-allowed. The module also evaluates a per-minute sliding rate-limit window, rotates keys with a grace period (rotateApiKey), and revokes them (revokeApiKey).

integrations/webhooks.ts drives outbound delivery. WebhookDelivery.status ∈ {pending, in-flight, success, failed, dead-letter}, and matchesTopicPattern supports three forms — an exact match, a prefix.* subtree match, and a bare * wildcard. Transient failures retry with exponential backoff plus jitter until maxRetries, then move to dead-letter; replayDelivery resets a delivery for operator-initiated replay from the webhook simulator route.

integrations/connector-registry.ts defines CONNECTOR_KINDS = ['lms', 'calendar','identity','payment','telemetry','byom-ingest','slack','teams']. This is the authoritative, coarser taxonomy: the prose elsewhere over-enumerates LMS sub-flavors (LTI 1.3, LTI Advantage, SCORM) and named calendar providers (Google, Apple, Outlook) as if they were distinct connector kinds. At the registry level, they are not. SCORM and the LTI variants are configuration sub-flavors of the single lms kind; named providers are configuration of the calendar kind. Each ConnectorRegistration carries an hourly error budget, a circuit breaker (evaluateCircuitBreaker opens on budget-exhausted), version pinning, and an upgradeConnector path that refuses unknown or deprecated versions.

Notifications, help center, status page#

notifications/notifications.ts defines NOTIFICATION_CHANNELS = ['in-app', 'push','email','sms','voice','webhook'] — and yes, voice is a real enum member, not aspirational. Severities are NOTIFICATION_SEVERITIES = ['info', 'standard','important','critical']. decideDelivery resolves per-channel preferences (with per-domain and per-persona overrides), quiet hours (overridden only by critical), bounce/suppression state, minimum-severity gates, and digestMode ∈ {off, hourly, daily, weekly} — returning an explicit per-channel verdict (deliver, skip-disabled, skip-quiet-hours, skip-below-severity, skip-bounced, skip-suppressed, defer-to-digest). Templates carry localized perLocale subject/body, required-variable validation, accessibility lint tags, and an approval state machine.

help-center/help-center.ts defines HELP_AUDIENCES = ['customer','admin', 'creator','partner'] and HELP_KINDS = ['article','walkthrough','video-script', 'faq','changelog']. searchHelp is role-aware: it filters by viewer audience and applicable roles, scores contextual matches against the current domain and surface path, and falls back across locales. A feedback loop (aggregateFeedback) rolls up helpful/unhelpful counts and missing-subject tallies to feed the editorial backlog.

status-page/status-page.ts defines COMPONENT_STATES = ['operational', 'degraded','partial-outage','major-outage','maintenance'] and rolls up component health to an overall and per-region state. An IncidentRecord carries a thread of publicUpdates with status ∈ {investigating, identified, monitoring, resolved}; banners are targeted by severity, audience tenant/role, and locale; and postmortems carry a visibility ∈ {public, tenant-scoped, internal} enforced by postmortemVisibleTo.


Developer portal — what "integration certification flows" actually means#

The integrations area's "developer documentation, OpenAPI specs, code examples, sandbox tenants, and integration certification flows" are backed by a real library, libs/oshun/developer-portal, whose src/index.ts exports five modules: openapi-builder, code-example-generator, sandbox-tenant-policy, certification-suite, and developer-docs-registry.

  • openapi-builder.ts generates an OpenAPI 3.1.0 document (openapi: '3.1.0') directly from Zod schemas via zodToOpenApiSchema, emitting both security schemes — so the public API docs stay in sync with the actual request/response contracts instead of drifting.
  • certification-suite.ts is the concrete "certification flow." It defines CERTIFICATION_CHECK_KINDS — twelve deterministic checks including auth-token-rotation (must keep authenticating within 60 minutes of a key rotation), webhook-signature-verification (must validate the X-Oshun-Signature HMAC-SHA256 header), webhook-replay-rejection, rate-limit-respect, idempotency-key-honor, pagination-cursor-correctness, partial-failure-envelope-parse, residency-region-honor, event-deduplication, oauth-pkce-on-public-client, scopes-least-privilege, and privacy-deletion-fanout. Each check has a gate level (required / recommended / optional) and a remediation URL; the suite reports a pass-rate and an overall gate verdict before an integration is listed as "Oshun-certified."
  • code-example-generator and sandbox-tenant-policy back the code examples and sandbox tenants surfaced in the docs.

The admin console exposes these at api/admin/developer-portal/articles, api/admin/developer-portal/certification/checks, and api/admin/developer-portal/sandbox-policies.


Oshun Admin Mobile#

Oshun Admin mobile V1 is a real Expo app (apps/oshun/admin-mobile, with app.json and eas.json). It is not a thin web wrapper — it has its own module tree under src/: urgent-queue, review, auth, incidents, offline, notifications, shell, support, and supporting accessibility, design, navigation, telemetry, and theme modules.

It includes a secure mobile admin shell, login, step-up auth, an urgent-queue home, an assignment overview, push-driven escalation, and low-connectivity-safe queue behavior. The home surface uses compact review cards with severity and SLA state. Where mobile action is safe, an operator can approve, reject, request changes, escalate, reassign, acknowledge an incident, handle high-priority support cases, and process creator/reviewer notifications. The experience is tuned for the constraints of a phone on call: compact evidence previews, incident summaries, degraded-connectivity and retry states, handoff states, and deliberate haptics, motion, and confirmation behavior. Coverage spans mobile E2E, accessibility, telemetry, device-specific visual QA, and urgent-queue signoff.

The security posture is visible in the module structure:

  • auth/adminMobileSessionController.ts, adminMobileStepUp.ts, and expoSecureKeyValueStorage.ts (session tokens go to the device secure store, and sensitive actions trigger a step-up challenge), plus the React context providers AdminMobileAuthContext.tsx and AdminMobileSessionRuntimeProvider.tsx.
  • review/ — separate queue, routing, decision, and evidence clients (adminMobileReviewQueueClient.ts, adminMobileReviewRoutingClient.ts, adminMobileReviewDecisionClient.ts, adminMobileReviewEvidence.ts).
  • notifications/ — an Expo escalation router (adminMobileEscalationRouter.ts) with a typed adminMobileEscalationPayload.ts and a creator/reviewer notification path.
  • urgent-queue/, incidents/, and offline/ — the urgent-triage home, incident acknowledgement, and low-connectivity behavior.

Honest status#

Capability Status
Admin web console (274 components, deep BFF route surface) Implemented
@oshun/tenant-console 9-module substrate (identity/roles/audit/bulk/integrations/notifications/help/status/tenant-model) Implemented (deterministic pure functions)
Hash-chained tamper-evident audit, per-tenant viewer scoping Implemented
Role diff + risk scoring + dry-run harness + recertification state machine Implemented
Bulk dry-run, residency counting, per-kind export matrix, signed manifests Implemented
API keys (prefix+hash), webhooks (topic-pattern/retry/dead-letter), connectors Implemented
Developer portal: OpenAPI 3.1 from Zod, 12-check certification suite Implemented
Admin mobile (Expo): urgent queue, step-up auth, escalation router, offline Implemented
SSO/SCIM as deterministic pure functions; OIDC live login Fail-closed (503) when oidcClientId/oidcJwksUrl absent — no live SAML/OIDC parser in lib
Tenant-admin SSO config UI (metadata-XML/OIDC-discovery parse, transform matrix, auth-policy panel, sandbox-probe verdicts) Partially covered end-to-end

No part of this area reads as fabricated. The two honest gaps are an intentional fail-loud OIDC seam and a partially-covered config UI — both stated above, neither a hidden stub.