Oper backend — business features

What the platform does, in plain words, with the routes, events and jobs behind each feature · 14 services · 456 HTTP routes · 94 NATS consumers · 74 published subjects · entry points scanned by tools/feature-docs/scan.py

What this is

One page per service, describing features — what a person or another service can actually do — and mapping each one to the HTTP routes, NATS subjects and background jobs that implement it. Every entry point carries a verdict: live (a caller was found), suspect (no caller in this repo; the admin web and mobile app are not in this repo), dead (no caller and unreachable).

Use the dead-code roll-up to decide what to delete. Documentation pages never delete code — that is a separate, per-service PR.

Platform in one picture

Solid arrows are synchronous HTTP (a user request is waiting). Dotted arrows are NATS events (fire-and-forget). Click a service to open its page.

flowchart LR
  FE["Admin web · Mobile app"]
  subgraph identity["Identity & tenancy"]
    UA["users-auth
+ otp · onboarding workers"] end subgraph learning["Learning & delivery"] ML["micro-learning
+ tts worker"] AS["assignment"] NW["notification"] FS["feed"] end subgraph authoring["Authoring"] AV2["authoring-v2
(hub)"] PIPE["content · image · extraction
research · suggest · rag-context"] end AM["asset-manager"] STT["stt"] FE --> UA UA --> ML UA --> AS UA --> NW UA --> AV2 UA -.-> OTPW["otp / onboarding"] ML --> AS ML --> UA AS --> NW AS --> ML ML -.-> NW ML -.-> TTSW["tts"] AV2 -.-> PIPE PIPE -.-> AV2 AV2 --> ML AV2 --> AM AV2 --> STT PIPE --> AM TTSW --> AM click UA "users-auth-service/docs/features.html" "users-auth-service" click ML "micro-learning-service-v2/docs/features.html" "micro-learning-service-v2" click AS "assignment-service/docs/features.html" "assignment-service" click NW "notification-worker/docs/features.html" "notification-worker" click FS "feed-service/docs/features.html" "feed-service" click AV2 "authoring-service-v2/docs/features.html" "authoring-service-v2" click AM "asset-manager-service/docs/features.html" "asset-manager-service" click STT "stt-service/docs/features.html" "stt-service"

How to read a service page

Every service page has the same nine sections, in the same order:

SectionWhat you get
1. What it isTwo sentences, plus the runtime facts (Deployment, database, streams, external APIs).
2. Feature mapOne diagram: who uses it → which features → which entry points.
3. FeaturesThe story. Grouped by actor: admin, employee (mobile), internal, background.
4. API referenceEvery route: method, full path, auth scope, feature, known callers, verdict.
5. Async contractsSubjects consumed and published, durables, background jobs.
6. Data it ownsTables, Redis, S3 — including tables another service writes into.
7. DependenciesWho calls it, what it calls.
8. Dead-code verdictsEverything with no caller, with the evidence.
9. SourcesThe markdown docs and audit reports the page is built from.

Core services

These five carry the user-facing product. They are also where the route count is concentrated.

users-auth-service

Identity for two audiences: admins signing into the web back-office and employees signing into the mobile app. Owns tenants, users, groups, locations, roles, credits and Stripe billing, and proxies the back-office to authoring. The only service on the public ingress.

147 routes · 7 NATS consumers · 20 published subjects · 5 jobs + 1 CronJob

micro-learning-service-v2

The learning content itself: modules, sections, lessons, screens, quizzes. Tracks employee progress, streaks, points and certificates, and drives narration audio through its own TTS worker.

109 routes · 2 NATS consumers · 14 published subjects · 11 jobs

assignment-service

Who has to do which training by when. Expands a target (a company, a group, a list of people) into individual recipients, then chases them with reminders and escalations until the due date.

61 routes · 7 NATS consumers · 14 published subjects · 9 jobs

notification-worker

Delivery. Turns events from the rest of the platform into push notifications and an in-app inbox, honouring per-user preferences, quiet hours and daily caps. Owns the device registry.

15 routes · 11 NATS consumers · 1 published subject · 2 jobs

authoring-service-v2

Where courses get made. A chat-driven authoring surface with 27 tools, an orchestrator that turns an uploaded document into a full module, and the publish path into micro-learning.

67 routes · 33 NATS consumers · 7 published subjects · 27 tools · 6 jobs

Authoring pipeline workers

Six background workers. They only ever talk to authoring-v2 — never to each other — so each one is a self-contained step.

authoring-content-worker

Writes the actual lesson content with an LLM: screens and blocks for each concept, plus translation into other languages.

2 health routes · 5 NATS consumers · 6 published subjects

authoring-image-worker

Generates lesson imagery with Gemini, one at a time or as an overnight batch, and hands the files to asset-manager.

4 health routes · 3 NATS consumers · 2 published subjects

extraction-worker

Reads an uploaded document and turns it into a lesson tree — the skeleton a module is built on.

3 health routes · 2 NATS consumers · 3 published subjects

research-worker

Builds a module outline from web research when there is no source document — topic in, structure out.

3 health routes · 2 NATS consumers · 3 published subjects

suggest-pipeline-worker

Outlines and suggestions for the authoring chat. Also the only service holding the OpenRouter account key, so it fronts credit-metered LLM calls for others.

5 routes · 2 NATS consumers · 2 published subjects

rag-context-worker

Indexes source documents into RAGFlow and serves the retrieved context back to whoever is generating content.

7 routes · 4 NATS consumers

Storage & support

feed-service

The social side: posts, reactions, recognition and a live feed over SSE.

21 routes · 16 NATS subscriptions · 1 published subject

asset-manager-service

Every file the platform stores. Hands out presigned upload URLs, keeps the asset registry, serves through the CDN.

9 routes · 1 published subject

stt-service

Speech to text. One internal endpoint, used when an author talks to the creation chat instead of typing.

3 routes

Libraries & satellites

libs/

The four shared packages every service depends on: auth, observability, block tooling and tenancy.

oper_auth · observability · oper_tools · oper_tenancy

Satellites

Load-bearing pieces that do not run in the cluster: the Modal GPU endpoints, the Cloudflare edge worker, the docs portal.

chatterbox-modal · miner-u · link-otp-worker · docs-portal

Business flows

The journeys that cross service boundaries. Each page has a sequence diagram, a numbered walk-through that links into the service pages, and a note on where the flow breaks today.

Login & onboarding

Admin sign-in, employee OTP, invitations, and what happens when a new company registers.

Document → module

Upload a PDF, get a drafted course: extraction → content generation → imagery.

Publish & audio

Publishing a draft into the learner-facing catalogue, and narrating it.

Assign & notify

Assign training to a group, expand it to people, push, remind, escalate.

Progress, streaks & certificates

What the mobile app reports as a learner works, and what the platform does with it.

Credits & billing

Stripe subscriptions, AI credit debits at run start and publish, and reconciliation.

Dead-code roll-up

Route verdicts, counted from each page's API table. Every row on every page also carries an evidence line; click through to the service's verdict section for the reasoning behind a suspect or dead call.

349 of 456 routes have a caller. 57 are suspect — no caller in this repo, but the admin web and mobile apps are not in this repo. 50 are dead: no caller AND unreachable in principle. The "other findings" column counts the non-route entry points (consumers, published subjects, background jobs, tables) that the page marks as not live.

ServiceRouteslivesuspectdeadOther findingsBiggest item
users-auth-service14712813632A shadowed duplicate route makes the filterable tenant-users handler unreachable; 9 published subjects nobody consumes
micro-learning-service-v21099955198 dead published subjects, including four per-lesson telemetry events stored and never read
assignment-service6122132615The router is mounted twice; only one half of each pair is reachable
notification-worker1513206The chat consumer and its NATSService are dead on both halves
authoring-service-v2675411211A publish nothing listens to leaves the non-DBOS fallback never indexing its document
feed-service2171318The event pipeline is never started, so all 16 consumer registrations and the SSE endpoint are dead
authoring-content-worker22001The v2 generation contract is a deliberate flag-gated rollback path — keep
authoring-image-worker42202Probes point at the weaker health handler
extraction-worker31202Liveness is deliberately exec true; the HTTP health routes are unused
research-worker31202Same probe shape as extraction
suggest-pipeline-worker55002The low-credit alarm and the suggestions-failed event have no matching consumer filter
rag-context-worker75204Delete and cancel have no publisher: document deletion never reaches RAGFlow and cancelled parses stay billable
asset-manager-service97203Small, healthy surface
stt-service33001Nothing dead
Total4563495750108

Read this before deleting anything

  • suspect is not a deletion list. The admin web app and the mobile app live in other repositories. A suspect route means "nothing in this repo calls it" — confirm against real traffic or the frontend teams first.
  • Some dead code is load-bearing to keep. content-worker's v2 contract is the documented rollback path off v3. assignment-service's notification_event_worker must stay unstarted — starting it would double-send every assignment push.
  • Some dead entries are bugs, not waste. rag-context-worker's delete and cancel consumers, suggest-pipeline's low-credit alarm and authoring-v2's reconciliation-drift event are all features that were built and never connected. Deleting them removes the intent; wiring them up is probably the right fix.
  • One deletion has a trap. assignment-service's duplicate /v1 mount is mostly dead, but removing the mount would also remove the one live employee route on it. Split the router instead.

Corrections to ARCHITECTURE.md found on the way

That document was the starting point for this work and is mostly accurate. Six things in it are now out of date; each is evidenced on the service page that found it.

SectionSaysActually
§4.1(3)Streak notifications can never fireFixed. The publisher appends .ten.<tenant>; both sides are five tokens. Verified from publisher and consumer independently.
§4.1(5)Feed's four social subjects have no producerThree have none. social.post.created is published by feed itself — from the pipeline that never starts.
§4.1(6)authoring-v2 points at a Redis Service that does not existNow points at users-auth-redis DB 9.
§3.2feed-service is zero-outboundIt calls users-auth for batch author profiles.
§5libs/ToDelete/ is byte-identical to libs/observabilityIt is not: 69 lines vs 90, missing the Sentry noise filter. Shadowing would silently regress error filtering.
§2miner-u ships a fallback k8s manifestThere is no miner-u/k8s/. Three places still reference it, so the documented rollback has nothing to roll back to.

Glossary

TenantOne customer company. Almost every table and every request is scoped to a tenant.
AdminA back-office user of the web app: creates content, assigns training, manages people. Also called educator for the content-only role.
EmployeeA learner using the mobile app.
ModuleA course. The unit that gets published, assigned and completed.
Section / lesson / screen / blockThe content tree inside a module, from coarse to fine. A block is one piece of content — a paragraph, an image, a quiz question.
DraftA module while it is still being authored, before it is published.
RunOne execution of an authoring pipeline — for example turning a document into a module. Consumes credits.
AssignmentAn instruction that a target must complete a module by a date. Expands into one recipient row per person.
CreditThe billing unit for AI work. Debited when a run starts, refunded if it fails.
Stream / subject / durableNATS JetStream terms. A subject is the event name, a stream stores subjects, a durable is a named consumer that remembers its position.

Method & tooling

Entry points are found mechanically, then grouped into features by hand:

flowchart LR
  A["scan.py
routes · subjects · jobs · tools"] --> B["group into features"] B --> C["cross-reference callers
across the repo"] C --> D{"caller found?"} D -->|"in-repo client, FE doc,
or scheduler"| L["live"] D -->|"tests only, or nothing"| S["suspect"] S -->|"unreachable, or
zero traffic"| X["dead"] L --> F["verdict + evidence
on the service page"] S --> F X --> F F --> G["this roll-up → cleanup PRs"] style L fill:#1b7f4b,color:#fff,stroke:none style S fill:#b7791f,color:#fff,stroke:none style X fill:#b23b3b,color:#fff,stroke:none
tools/feature-docs/scan.pyStdlib AST scanner. Resolves route prefixes through include_router, and subjects through settings fields, local variables and for subject in … loops. Needs Python 3.11+. Output: tools/feature-docs/out/<service>.json.
tools/feature-docs/check.pyFails if a scanned route, subject or tool is missing from a page, if verdict badges are absent, or if a relative link is broken.
docs-shared/features.cssThe one stylesheet. features.js loads Mermaid from a CDN — diagrams need network, the rest of the page does not.
Templatedocs-shared/TEMPLATE-service-features.html — copy it to start a new service page.

Scanner limitations, stated so nobody over-trusts a count: subjects built at runtime from non-literal values are missed, and a route reachable only through a dynamically mounted router may be missed. Both show up as an entry point present in code but absent from the JSON, never the reverse — so check.py can under-report, not over-report.