authoring-service-v2 — business features

Where courses get made: the authoring API, the creation chat, and the hub that drives six AI workers · ← platform hub · entry points verified against tools/feature-docs/out/authoring-service-v2.json

1. What it is

This is where courses get made. An author works in the admin web app — typing a prompt, uploading a document, or editing a tree of sections and lessons by hand — and this service turns that into a module, farming the expensive AI work out to six background workers and collecting their results as they land.

It is a hub: all six pipeline workers (content, image, extraction, research, suggest, rag) talk only to it and never to each other (ARCHITECTURE.md §3.1). Nothing a learner sees lives here — publishing hands the finished module to micro-learning-service-v2, which serves it to the mobile app.

Prefix trap

This service exposes its internal surface at /internal/v1/*. Every other service in the platform uses /v1/internal/*. Callers that assume the platform convention get a 404 (ARCHITECTURE.md §3.2, last line). Its own outbound calls do use the other services’ /v1/internal/* paths, so both spellings appear in one file (app/core/http.py:56 calls /v1/internal/modules/ingest).

Who uses itAdmin web app and the back-office console, both arriving through users-auth’s /v1/oper-admin/authoring/* catch-all proxy (H22); micro-learning and users-auth over the internal API; six NATS pipeline workers; asset-manager; its own in-process loops
RuntimeFastAPI + WebSocket + 30 NATS subscriptions + DBOS durable workflows · Deployment authoring-service-v2, replicas: 1 (k8s/deployment.yaml:4,9)
DatabasePostgres authoring_service_v2_db (k8s/es-db-authoring-service-v2.yaml:26). DBOS keeps its own dbos schema in the same database (app/workflows/dbos_runtime.py:46-48). content-worker writes drafts.state directly (ARCHITECTURE.md §3.4)
Redisusers-auth-redis DB 9 — quota and budget caches (k8s/configmap.yaml:23). ARCHITECTURE.md §3.5 / §4.1(6) records the earlier value, a redis-service.oper Service that never existed; the configmap comment (k8s/configmap.yaml:11-19) documents what that broke
NATS streamsReads AUTHORING_RAG, AUTHORING_SUGGEST, AUTHORING_CONTENT, AUTHORING_EXTRACT, AUTHORING_RESEARCH, AUTHORING (image), AUTHORING_USAGE; owns and creates AUTHORING_CORE (authoring.asset.>, authoring.chat.>, authoring.dlq.>app/consumers/manager.py:54-55). Durables are asv2-<subject>, ack_wait=120, max_deliver=5 (manager.py:68-71,198-206)
External APIsNone directly. All LLM spend is proxied: suggest-pipeline-worker owns the OpenRouter key, asset-manager owns S3/CDN, and dictation goes to the in-cluster stt-service faster-whisper pod (app/core/http.py:714-750) — an HTTP edge not among the 27 in ARCHITECTURE.md §3.2
Entry points67 HTTP routes (66 REST + 1 WebSocket) · 27 tool handlers behind one of them · 33 NATS consumer registrations over 31 subjects · 16 published subjects · 6 background jobs

2. Feature map

flowchart LR
  ADM["Author in the admin web app"] --> CHAT["Creation chat"]
  ADM --> BUILD["The builder: 27 tools"]
  ADM --> RUNS["Generation runs, review gate, estimates"]
  ADM --> MKT["Marketplace: browse, copy, curate"]
  ADM --> CRED["AI credits and budget"]
  CHAT --> HTTP["48 routes under /v1/admin/authoring"]
  BUILD --> TOOLS["POST /tools/{tool_name}"]
  RUNS --> WS["WS /v1/admin/authoring/ws"]
  MKT --> HTTP
  CRED --> HTTP
  SVC["micro-learning, users-auth"] --> INT["16 routes under /internal/v1"]
  WORK["6 pipeline workers"] -.-> EVT["31 consumed NATS subjects"]
  EVT -.-> RUNS
  LOOPS["In-process loops"] --> JOBS["6 background jobs"]

3. Features

Admin

Creation chat live

admin

An author describes the course they want — or drops in a document — and the service answers with a short conversation: a few setup questions, a list of topics to confirm, then a module being written in front of them. Each answer is a card in a thread they can leave and come back to. The thread is the record: what was asked, what was confirmed, and which run it started.

Entry points
POST/GET /v1/admin/authoring/creation-chats, GET /v1/admin/authoring/creation-chats/{chat_id}, POST /v1/admin/authoring/creation-chats/{chat_id}/messages
Touches
creation_chats, creation_chat_messages, course_generation_runs; suggest-pipeline-worker for the setup questions (app/core/http.py:423)
Related
New-Design/SDD-creation-chat.md, New-Design/PLAN-doc-creation-chat.md, docs/fe-creation-chat.md
Evidence
app/routes/creation_chat_routes.py:54,95,123,143 · caller users-auth-service/app/routes/oper_authoring.py:369 · docs/fe-creation-chat.md:49

Voice dictation live

admin

Instead of typing the prompt, the author holds the mic button and speaks. The recording is forwarded to an in-cluster speech-to-text pod and the transcript comes back into the prompt box. Nothing is stored: the audio passes through.

Entry points
POST /v1/admin/authoring/transcriptions
Touches
stt-service (faster-whisper, /v1/internal/transcriptions)
Related
docs/fe-voice-dictation.md, docs/API_ENDPOINTS.md §9
Evidence
app/routes/transcription_routes.py:37 · app/core/http.py:714-750 · docs/fe-voice-dictation.md:13

Live updates over the WebSocket live

admin

Generation takes minutes, so the screen does not poll. The browser opens one socket, subscribes to the draft, document, chat or run it cares about, and receives progress, “lesson written”, “image ready” and terminal frames as the workers report in. A reconnect replays the last few minutes so nothing is missed.

Entry points
WS /v1/admin/authoring/ws (token + tenant_id in the query string)
Touches
ws_replay_events; every NATS consumer publishes into the broker
Related
docs/frontend-integration.md §6, docs/fe-progress-integration.md
Evidence
app/routes/ws_routes.py:33 · token minted by users-auth-service/app/routes/oper_authoring.py:300-313 · known defect: regenerate-block frames leaked across tenants, fixed (bug-hunt-reports/authoring-service-v2.md #1)

The builder — 27 tools live

admin

Every editing action in the UI is one named tool posted to one route, and every call is recorded with its input, output and who made it. The tools do six kinds of thing: manage the module itself (create, rename, duplicate), change its catalog state (archive, unpublish, delete), edit the tree (sections, lessons, screens, blocks, moves and reorders), roll a draft back to an earlier version, publish or translate, and start or cancel a generation run.

Entry points
POST /v1/admin/authoring/tools/{tool_name} (27 handlers), GET /v1/admin/authoring/tools, GET /v1/admin/authoring/tool-calls, GET /v1/admin/authoring/tool-calls/{tool_call_id}
Touches
modules, drafts, draft_versions, tool_calls; the 8 async catalog entries publish to the workers instead of running here
Related
docs/frontend-integration.md §3-4, docs/API_ENDPOINTS.md §2-3, libs/oper-tools/oper_tools/catalog.json (35 entries: these 27 sync + 8 async)
Evidence
app/routes/tool_routes.py:67 · app/tools/register.py:26-70 · per-tool scope app/tools/scopes.py:32-53
ToolGroupWhat it achievesRegistered at
create-moduleModule shelfStart a new module and its empty draft.app/tools/register.py:26
update-moduleModule shelfRename it, change description, category, difficulty, goal, skills, cover image. Allow-listed fields only (bug-hunt #11).app/tools/register.py:27
duplicate-moduleModule shelfCopy a module and its whole tree into a new draft.app/tools/register.py:28
archive-moduleCatalog stateTake it off the shelf without deleting it.app/tools/register.py:32
unarchive-moduleCatalog statePut an archived module back.app/tools/register.py:33
unpublish-moduleCatalog statePull the published version out of micro-learning.app/tools/register.py:34
delete-moduleCatalog stateDestroy the module and its versions. Irreversible.app/tools/register.py:35
create-sectionTree editAdd a section to the draft.app/tools/register.py:38
update-sectionTree editRetitle or re-describe a section.app/tools/register.py:39
delete-sectionTree editRemove a section and everything under it.app/tools/register.py:40
create-lessonTree editAdd a lesson to a section.app/tools/register.py:41
update-lessonTree editEdit lesson title, duration, type, requirements.app/tools/register.py:42
delete-lessonTree editRemove a lesson.app/tools/register.py:43
create-screenTree editAdd a screen to a lesson.app/tools/register.py:44
delete-screenTree editRemove a screen.app/tools/register.py:45
create-blockTree editAdd a content block (text, quiz, divider, image, …) to a screen.app/tools/register.py:46
update-blockTree editEdit a block in place.app/tools/register.py:47
delete-blockTree editRemove a block.app/tools/register.py:48
move-lessonTree editMove a lesson to another section.app/tools/register.py:49
reorderTree editReorder sections, lessons, screens or blocks.app/tools/register.py:50
restore-draft-versionVersioningRoll the draft back to an earlier snapshot.app/tools/register.py:53
publish-modulePublishingSend the draft to micro-learning as a new version and make it live.app/tools/register.py:56
translate-modulePublishingRetrofit a translation of an already-published module.app/tools/register.py:58
upload-documentSource documentsGet an upload URL from asset-manager and register the source file.app/tools/register.py:59
request-module-from-docGeneration runStart the document→module run.app/tools/register.py:63
request-module-from-promptGeneration runStart the research→module run from a prompt.app/tools/register.py:66
cancel-runRun controlStop a run and tell the workers to drop pending work.app/tools/register.py:70

Drafts and version history live

admin

The draft is the live tree an author edits; every mutation snapshots the previous state. An author can read the tree, list the snapshots, open one, and roll back to it. Reads are separate from edits: the tree is fetched over REST, never pushed down the socket.

Entry points
GET /v1/admin/authoring/drafts/{draft_id}, GET /v1/admin/authoring/drafts/by-module/{module_id}, GET /v1/admin/authoring/drafts/{draft_id}/versions, GET /v1/admin/authoring/drafts/{draft_id}/versions/{version_no}, tool restore-draft-version
Touches
drafts, draft_versions, rag_doc_refs; rag-context-worker for the source-document context blob (H5)
Related
docs/frontend-integration.md §4.3, §5.2
Evidence
app/routes/draft_routes.py:109,127,145,160 · docs/API_ENDPOINTS.md:200-203

Module shelf live

admin

The list of everything this tenant has authored, with its stage and status, plus one module’s detail and the runs that produced it. This is the screen an author lands on.

Entry points
GET /v1/admin/authoring/modules, GET /v1/admin/authoring/modules/{module_id}, GET /v1/admin/authoring/modules/{module_id}/runs
Touches
modules, course_generation_runs
Related
docs/frontend-authoring-flow.md, docs/create-draft-module.md
Evidence
app/routes/module_routes.py:56,83,99 · docs/API_ENDPOINTS.md:162-163

Document to module live

admin

An author uploads a policy, handbook or deck and asks for a course. The service registers a run, charges the estimate against the tenant’s AI credits, and walks the document through indexing, extraction, outlining, per-lesson writing and illustration — each step performed by a different worker and awaited as an event. The author watches one progress bar and ends up with a draft they can edit.

Entry points
tools upload-document and request-module-from-doc; GET /v1/admin/authoring/course-generation-runs, GET /v1/admin/authoring/course-generation-runs/{run_id}
Touches
course_generation_runs, drafts, rag_doc_refs, extraction_*; asset-manager (upload), rag-context-worker, extraction-worker, suggest-pipeline-worker, content-worker, image-worker
Related
docs/SDD-OPER-course-extraction-updated.md, New-Design/SDD-composer-engine.md, docs/worker-integration.md, ARCHITECTURE.md §3.3 (“Doc→module”)
Evidence
app/workflows/doc_to_module.py:142-176 · app/tools/handlers/orchestrator_handlers.py · app/routes/run_routes.py:26,52 · docs/fe-import-estimate.md:21

Extraction review gate live

admin

When extraction is not confident it covered the document faithfully, the run stops and hands the author a queue of flagged items. They fix or explicitly override each one — both are recorded with who and why — and then release the run, which publishes the structure request and lets writing continue. The gate is a feature, not an error.

Entry points
GET …/course-generation-runs/{run_id}/extraction, GET …/extraction/review-items, POST …/extraction/review-items/{item_id}, POST …/extraction/proceed
Touches
extraction_runs, extraction_review_items, extraction_ledger; publishes authoring.extract.structure.requested
Related
docs/SDD-OPER-course-extraction-ADDENDUM-integration.md, docs/fe-progress-integration.md, New-Design/PLAN-doc-creation-chat.md §5
Evidence
app/routes/extraction_routes.py:1-6,37,73,120,153 · app/services/extraction_orchestration.py:635 · docs/fe-progress-integration.md:150

Research to module live

admin

With no document to start from, the author types what the course should teach. A research worker searches the web, comes back with a topic list, and the author confirms or trims it before any writing is paid for. Confirmation triggers the deep pass, which returns an outline that is materialised into a draft and written lesson by lesson.

Entry points
tool request-module-from-prompt; GET …/{run_id}/research, GET …/research/topics, POST …/research/topics/confirm, GET …/research/outline
Touches
research_runs, research_topics, course_generation_runs, drafts; research-worker then content-worker
Related
docs/fe-research-module.md, ARCHITECTURE.md §3.3 (“Research→module”)
Evidence
app/routes/research_routes.py:52,80,104,127 · app/services/research_orchestration.py:76,564 · docs/fe-research-module.md:63

Cost estimates before a run live

admin

Generation costs real money, so the author sees the price first: how many credits a given document or prompt is likely to cost, and — for a prompt — a few clarifying questions that make the estimate and the result better. Nothing is charged until they start the run.

Entry points
GET /v1/admin/authoring/assets/{asset_id}/import-estimate, GET /v1/admin/authoring/prompt-import-estimate, POST /v1/admin/authoring/prompt-import-context, GET /v1/admin/authoring/modules/{module_id}/narration-estimate
Touches
course_generation_runs.cost_estimate; suggest-pipeline-worker (/v1/internal/prompt-context-questions, 60s timeout); micro-learning narration quote
Related
docs/fe-import-estimate.md, docs/fe-research-module.md, docs/module-translations-fe.md
Evidence
app/routes/estimate_routes.py:38,85,175 · app/routes/module_routes.py:135 · app/core/http.py:83,423

AI credits and budget live

admin

Every AI call the platform makes for this tenant is logged with what it cost. An admin sees the month’s spend, the individual events, what one run cost, and how close they are to their cap.

Entry points
GET /v1/admin/authoring/credits/summary, GET …/credits/events, GET …/credits/runs/{run_id}, GET …/credits/budget
Touches
usage_events, tenant_budgets, Redis DB 9 counters; users-auth holds the credit balance
Related
docs/ai-cost-usage-plan.md, docs/ai-cost-monitoring.md
Evidence
app/routes/ai_usage_routes.py:126,189,213,237 · docs/ai-cost-usage-plan.md:169-172

Publish and translate live

admin

Publishing is the moment a draft becomes a course learners can open: the tree is sent to micro-learning as a new version and flipped live, and the local module is stamped published. Translating a published module retrofits another language without re-authoring it.

Entry points
tools publish-module, unpublish-module, translate-module, archive-module, unarchive-module, delete-module
Touches
modules, marketplace_modules (publishing in the office is what lists a card); micro-learning ingest/publish/unpublish/translations (H1); publishes authoring.content.translate.requested
Related
docs/fe-module-lifecycle.md, docs/module-translations-fe.md
Evidence
app/services/publish_service.py:158-208,376 · app/tools/handlers/translate_handlers.py:97 · app/core/http.py:52-243 · known defects: chat-driven publish can skip the live flip and duplicate versions on a narrow retry (bug-hunt-reports/authoring-service-v2.md #3, #5, deferred)

Marketplace — browse and copy live

admin

A tenant admin browses a shared catalogue of ready-made modules, previews one, and copies it into their own tenant as an editable draft. The copy is theirs: the target tenant comes from their token, never from the request body.

Entry points
GET /v1/admin/authoring/marketplace/modules, GET …/marketplace/modules/{catalog_id}, GET …/marketplace/modules/{catalog_id}/preview, POST …/marketplace/modules/{catalog_id}/copy
Touches
marketplace_modules, marketplace_categories, modules, drafts; asset-manager for image re-hosting on copy-in
Related
docs/marketplace-plan.md §3-4
Evidence
app/routes/marketplace_routes.py:126,199,227,253 · docs/marketplace-plan.md:60,233-235 · known defect: pages past the 500-row ranking cap come back empty while total still counts them (bug-hunt #14, deferred)

Marketplace — office curation live

admin

One internal tenant — the office — owns the catalogue. Its operators classify cards (category, tags, featured, author name), pull a card without unpublishing the module, and copy a customer’s module into the office as a draft to curate. Any other tenant asking for these screens gets a 403.

Entry points
GET/PATCH …/marketplace/listings[/{catalog_id}], GET /v1/admin/authoring/tenants/{source_tenant_id}/modules, GET …/{source_module_id}/preview, POST …/{source_module_id}/copy
Touches
marketplace_modules, modules, drafts; asset-manager POST /api/v1/assets/copy
Related
docs/marketplace-plan.md §3 (“Internal office”), §4a
Evidence
app/routes/marketplace_routes.py:366-374 (office guard), 378,401,414,462,504,533 · docs/marketplace-plan.md:194-198

Categories live

admin

The single axis a module is filed under at publish. An admin picks from the platform list or adds a private one for their tenant; only the office can edit the shared rows. Deactivating a category hides it from the picker without moving the modules already filed under it.

Entry points
GET /v1/admin/authoring/categories, POST /v1/admin/authoring/categories, PATCH /v1/admin/authoring/categories/{category_id}
Touches
marketplace_categories, category_synonyms
Related
docs/API_ENDPOINTS.md §10, docs/marketplace-plan.md §1
Evidence
app/routes/marketplace_routes.py:278,305,327 · docs/API_ENDPOINTS.md:373-375

Employee (mobile)

None. A learner never reaches this service. Published modules are served by micro-learning-service-v2; this service only hands them over at publish (H1).

Internal (other services)

Recent-modules card for micro-learning live

internal

Micro-learning’s admin dashboard shows “recently authored” modules. It asks this service for them rather than keeping a copy.

Entry points
GET /internal/v1/tenants/{tenant_id}/modules/recent
Touches
modules
Related
ARCHITECTURE.md §3.2 H14
Evidence
app/routes/internal_routes.py:492 · caller micro-learning-service-v2/app/clients/authoring_client.py:32

Back-office run inbox live

internal

Oper staff watch generation across all tenants from the back-office console: how the pipeline is doing, which runs are in flight, and what happened inside one run. users-auth aggregates it; the data comes from here.

Entry points
GET /internal/v1/pipeline/stats, GET /internal/v1/runs, GET /internal/v1/runs/{run_id}
Touches
course_generation_runs, tool_calls
Related
ARCHITECTURE.md §3.2 H22, docs/API_ENDPOINTS.md §8
Evidence
app/routes/internal_routes.py:192,314,375 · caller users-auth-service/app/clients/backoffice_clients.py:229,268,294

Ops surfaces dead

internal

Twelve key-only endpoints exist for an operator with a terminal: pipeline health, DBOS workflow listing/cancel/resume, on-demand credit reconciliation and tenant budget overrides, and the bilingual category-synonym table. Nothing in this repo calls any of them — the nightly loops do the same work unattended — so they are reachable only by hand.

Entry points
GET /internal/v1/pipeline/health; /internal/v1/dbos/workflows* (4); /internal/v1/credits/* (4); /internal/v1/marketplace/category-synonyms* (3)
Touches
reconciliation_runs, tenant_budgets, category_synonyms, the DBOS system schema
Related
docs/ai-cost-monitoring.md (runbook), docs/API_ENDPOINTS.md §8, docs/marketplace-plan.md §3
Evidence
see §8 — each row states what was searched

Background

The doc→module orchestrator run live

background

One run is a durable workflow, not a request: it survives pod restarts, resumes where it stopped, and publishes each step exactly once. It indexes the document, retrieves context, asks for an outline, fans out one writing job per concept, waits for them all, then requests illustrations and closes the run.

Entry points
DBOS workflow started by tool request-module-from-doc; inspected via /internal/v1/dbos/workflows*
Touches
course_generation_runs, drafts, the dbos schema; publishes authoring.rag.context.requested, authoring.suggest.outline.requested, authoring.content.generation.requested
Related
New-Design/SDD-composer-engine.md, docs/worker-integration.md
Evidence
app/workflows/doc_to_module.py:157-176,213-215,356,376,414 · started from app/main.py:183 (init_dbos) · without DBOS the fallback path publishes to a subject nobody consumes — see §8

Stall sweeper and chat housekeeping live

background

If a worker’s final event is lost, the run would hang forever and the author would stare at a frozen progress bar. Every two minutes a sweep parks such runs as finished-with-errors, and the same pass closes abandoned creation chats and nudges ones waiting on an answer.

Entry points
_run_stall_sweep_loop (120s)
Touches
course_generation_runs, research_runs, creation_chats
Related
New-Design/SDD-creation-chat.md §6
Evidence
app/main.py:128-160,190 · app/services/run_stall_sweeper.py:67,143 · app/services/creation_chat_sweeper.py

AI credit accounting live

background

A run is paid for up front: the estimate is debited from the tenant’s credits before any worker starts. If the run fails the debit is refunded; if it succeeds the charge is trued up against what the work actually cost. A sweep every fifteen minutes catches refunds and true-ups whose inline attempt was lost, and a nightly audit compares the ledger against the provider’s own numbers and reports the drift.

Entry points
_settlement_sweep_loop (900s), _reconciliation_loop (daily, 300s after boot), consumer authoring.usage.event, UsageService.bump_cache
Touches
usage_events, tenant_budgets, reconciliation_runs, Redis DB 9; users-auth credit debit/refund (H4); suggest-pipeline’s OpenRouter proxies (H3)
Related
docs/ai-cost-usage-plan.md §7-8, docs/ai-cost-monitoring.md
Evidence
app/services/credit_gate.py:51,100,149,250 · app/main.py:163-177,187,193 · app/core/http.py:633,656 · app/services/reconciliation_service.py:202 · known defect: the cache bump is an unreferenced task and can be collected before it runs (bug-hunt #10, deferred)

Worker event ingestion live

background

Thirty subscriptions turn worker events into visible progress: each one advances the run, writes what arrived into the draft, and pushes a frame to whoever is watching. They pull from JetStream with a durable cursor, so a restart or a dropped connection delays events instead of losing them, and a duplicate delivery is deduplicated before it can double-write.

Entry points
30 registrations in app/consumers/register.py, one pull loop per subject
Touches
course_generation_runs, drafts, processed_events, ws_replay_events
Related
docs/worker-integration.md, ARCHITECTURE.md §3.3
Evidence
app/consumers/register.py:21-73 · app/consumers/manager.py:194-225 · started from app/main.py:184-185 · known defects: one failed concept no longer fails the whole run (bug-hunt #12, fixed); the chat consumer still has no dedup (bug-hunt #4, deferred)

Uploaded document to RAG index live

background

When asset-manager confirms a source document finished uploading, this service immediately asks the RAG worker to index it, so the text is searchable by the time the author starts a run.

Entry points
consumer authoring.asset.upload.completed → publish authoring.rag.indexing.requested
Touches
rag_doc_refs
Related
ARCHITECTURE.md §3.3 (AUTHORING_CORE)
Evidence
app/consumers/asset_consumer.py:83 · publisher asset-manager-service/app/api/v1/assets.py:52 · consumer rag-context-worker/app/consumers/indexing_consumer.py:194

Admin notifications live

background

Batch illustration can take hours, so nobody keeps the tab open. When a run finishes — or its images do — this service hands notification-worker a ready-made message with a deep link back to the module.

Entry points
publish training.notifications.ten.<tenant>.admin.<type>
Touches
notification-worker’s admin inbox and push delivery
Related
New-Design/SDD-admin-notification-centre.md
Evidence
app/publishers/admin_notifications.py:220-225 · consumer filter notification-worker/app/core/config.py:115 (training.notifications.ten.*.admin.>)

Retention sweep live

background

Two tables would otherwise grow without limit: the WebSocket replay buffer and the event dedup ledger. Once a minute the expired rows are deleted — small, indexed deletes that also keep the replay read fast. Creation-chat retention is handled on the stall-sweep cadence.

Entry points
_retention_loop (60s)
Touches
ws_replay_events (2×TTL), processed_events (7 days)
Related
New-Design/SDD-creation-chat.md §6, migrations/versions/0023_perf_indexes.py
Evidence
app/main.py:63-68,71-106,186

4. API reference

All 67 routes from the scan. Every /v1/admin/authoring/* route has an in-repo caller by construction: users-auth proxies the entire prefix verbatim through one catch-all (users-auth-service/app/routes/oper_authoring.py:369-398, ARCHITECTURE.md §3.2 H22), and the listed frontend doc names the specific path. The admin web app itself is not in this repo.

MethodPathAuthFeatureCallersVerdict
GET/health/livenone (unauthenticated)Health probesk8s livenessProbe k8s/deployment.yaml:68live
GET/health/readynone (unauthenticated)Health probesk8s readinessProbe k8s/deployment.yaml:75live
POST/internal/v1/credits/reconcileX-Oper-Key (internal)Ops surfacesnone in repo — runbook curl only, docs/ai-cost-monitoring.md:95dead
GET/internal/v1/credits/reconciliation-runsX-Oper-Key (internal)Ops surfacesnone in repo — runbook curl only, docs/ai-cost-monitoring.md:95dead
GET/internal/v1/credits/tenantsX-Oper-Key (internal)Ops surfacesnone in repo — runbook curl only, docs/ai-cost-monitoring.md:95dead
PUT/internal/v1/credits/tenants/{tenant_id}/budgetX-Oper-Key (internal)Ops surfacesnone in repo — runbook curl only, docs/ai-cost-monitoring.md:95dead
GET/internal/v1/dbos/workflowsX-Oper-Key (internal)Ops surfacesnone in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304dead
GET/internal/v1/dbos/workflows/{workflow_id}X-Oper-Key (internal)Ops surfacesnone in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304dead
POST/internal/v1/dbos/workflows/{workflow_id}/cancelX-Oper-Key (internal)Ops surfacesnone in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304dead
POST/internal/v1/dbos/workflows/{workflow_id}/resumeX-Oper-Key (internal)Ops surfacesnone in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304dead
GET/internal/v1/marketplace/category-synonymsX-Oper-Key (internal)Ops surfacesnone in repo — docs/marketplace-plan.md:168dead
POST/internal/v1/marketplace/category-synonymsX-Oper-Key (internal)Ops surfacesnone in repo — docs/marketplace-plan.md:168dead
PATCH/internal/v1/marketplace/category-synonyms/{synonym_id}X-Oper-Key (internal)Ops surfacesnone in repo — docs/marketplace-plan.md:168dead
GET/internal/v1/pipeline/healthX-Oper-Key (internal)Ops surfacesnone in repo — docs/API_ENDPOINTS.md:297dead
GET/internal/v1/pipeline/statsX-Oper-Key (internal)Back-office run inboxusers-auth app/clients/backoffice_clients.py:229live
GET/internal/v1/runsX-Oper-Key (internal)Back-office run inboxusers-auth backoffice_clients.py:268live
GET/internal/v1/runs/{run_id}X-Oper-Key (internal)Back-office run inboxusers-auth backoffice_clients.py:294live
GET/internal/v1/tenants/{tenant_id}/modules/recentX-Oper-Key (internal)Recent-modules cardmicro-learning app/clients/authoring_client.py:32 (H14)live
GET/metricsnone (unauthenticated)Prometheus metricsnone in repo — no ServiceMonitor / scrape annotation anywheresuspect
GET/v1/admin/authoring/assets/{asset_id}/import-estimateadmin JWT · admin.panelCost estimatesusers-auth proxy oper_authoring.py:369 · docs/fe-import-estimate.md:12live
GET/v1/admin/authoring/categoriesadmin JWT · admin.panelCategoriesusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:373live
POST/v1/admin/authoring/categoriesadmin JWT · admin.panelCategoriesusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:373live
PATCH/v1/admin/authoring/categories/{category_id}admin JWT · admin.panel (+ office for platform rows)Categoriesusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:375live
GET/v1/admin/authoring/course-generation-runsadmin JWT · admin.panelDocument to moduleusers-auth proxy oper_authoring.py:369 · docs/frontend-integration.md:437live
GET/v1/admin/authoring/course-generation-runs/{run_id}admin JWT · admin.panelDocument to moduleusers-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:23live
GET/v1/admin/authoring/course-generation-runs/{run_id}/extractionadmin JWT · admin.panelExtraction review gateusers-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150live
POST/v1/admin/authoring/course-generation-runs/{run_id}/extraction/proceedadmin JWT · content.writeExtraction review gateusers-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150live
GET/v1/admin/authoring/course-generation-runs/{run_id}/extraction/review-itemsadmin JWT · admin.panelExtraction review gateusers-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150live
POST/v1/admin/authoring/course-generation-runs/{run_id}/extraction/review-items/{item_id}admin JWT · content.writeExtraction review gateusers-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150live
GET/v1/admin/authoring/course-generation-runs/{run_id}/researchadmin JWT · admin.panelResearch to moduleusers-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:142live
GET/v1/admin/authoring/course-generation-runs/{run_id}/research/outlineadmin JWT · admin.panelResearch to moduleusers-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:122live
GET/v1/admin/authoring/course-generation-runs/{run_id}/research/topicsadmin JWT · admin.panelResearch to moduleusers-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:92live
POST/v1/admin/authoring/course-generation-runs/{run_id}/research/topics/confirmadmin JWT · content.writeResearch to moduleusers-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:103live
GET/v1/admin/authoring/creation-chatsadmin JWT · admin.panelCreation chatusers-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:226live
POST/v1/admin/authoring/creation-chatsadmin JWT · content.writeCreation chatusers-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:49live
GET/v1/admin/authoring/creation-chats/{chat_id}admin JWT · admin.panelCreation chatusers-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:230live
POST/v1/admin/authoring/creation-chats/{chat_id}/messagesadmin JWT · content.writeCreation chatusers-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:175live
GET/v1/admin/authoring/credits/budgetadmin JWT · admin.panelAI credits and budgetusers-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:172live
GET/v1/admin/authoring/credits/eventsadmin JWT · admin.panelAI credits and budgetusers-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:171live
GET/v1/admin/authoring/credits/runs/{run_id}admin JWT · admin.panelAI credits and budgetusers-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:170live
GET/v1/admin/authoring/credits/summaryadmin JWT · admin.panelAI credits and budgetusers-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:169live
GET/v1/admin/authoring/drafts/by-module/{module_id}admin JWT · admin.panelDrafts and versionsusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:201live
GET/v1/admin/authoring/drafts/{draft_id}admin JWT · admin.panelDrafts and versionsusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:200live
GET/v1/admin/authoring/drafts/{draft_id}/versionsadmin JWT · admin.panelDrafts and versionsusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:202live
GET/v1/admin/authoring/drafts/{draft_id}/versions/{version_no}admin JWT · admin.panelDrafts and versionsusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:203live
GET/v1/admin/authoring/marketplace/listingsadmin JWT · office tenant onlyMarketplace curationusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:194live
GET/v1/admin/authoring/marketplace/listings/{catalog_id}admin JWT · office tenant onlyMarketplace curationusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:194live
PATCH/v1/admin/authoring/marketplace/listings/{catalog_id}admin JWT · content.publish · office tenant onlyMarketplace curationusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:195live
GET/v1/admin/authoring/marketplace/modulesadmin JWT · admin.panelMarketplace browse and copyusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:60live
GET/v1/admin/authoring/marketplace/modules/{catalog_id}admin JWT · admin.panelMarketplace browse and copyusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:233live
POST/v1/admin/authoring/marketplace/modules/{catalog_id}/copyadmin JWT · content.writeMarketplace browse and copyusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:235live
GET/v1/admin/authoring/marketplace/modules/{catalog_id}/previewadmin JWT · admin.panelMarketplace browse and copyusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:234live
GET/v1/admin/authoring/modulesadmin JWT · admin.panelModule shelfusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:162live
GET/v1/admin/authoring/modules/{module_id}admin JWT · admin.panelModule shelfusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:162live
GET/v1/admin/authoring/modules/{module_id}/narration-estimateadmin JWT · admin.panelPublish and translateusers-auth proxy oper_authoring.py:369 · docs/module-translations-fe.md:157live
GET/v1/admin/authoring/modules/{module_id}/runsadmin JWT · admin.panelModule shelfusers-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:163live
POST/v1/admin/authoring/prompt-import-contextadmin JWT · content.writeCost estimatesusers-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:34live
GET/v1/admin/authoring/prompt-import-estimateadmin JWT · admin.panelCost estimatesusers-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:20live
GET/v1/admin/authoring/tenants/{source_tenant_id}/modulesadmin JWT · office tenant onlyMarketplace curationusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:196live
POST/v1/admin/authoring/tenants/{source_tenant_id}/modules/{source_module_id}/copyadmin JWT · content.write · office tenant onlyMarketplace curationusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:198live
GET/v1/admin/authoring/tenants/{source_tenant_id}/modules/{source_module_id}/previewadmin JWT · office tenant onlyMarketplace curationusers-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:198live
GET/v1/admin/authoring/tool-callsadmin JWT · admin.panelThe builder (27 tools)users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:85live
GET/v1/admin/authoring/tool-calls/{tool_call_id}admin JWT · admin.panelThe builder (27 tools)users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:94live
GET/v1/admin/authoring/toolsadmin JWT · admin.panelThe builder (27 tools)users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:52live
POST/v1/admin/authoring/tools/{tool_name}admin JWT · per-tool scopeThe builder (27 tools)users-auth proxy oper_authoring.py:369 · docs/frontend-integration.md:108live
POST/v1/admin/authoring/transcriptionsadmin JWT · content.writeVoice dictationusers-auth proxy oper_authoring.py:369 · docs/fe-voice-dictation.md:13live
WS/v1/admin/authoring/wsadmin JWT in query stringLive updates (WebSocket)console browser direct, token from users-auth oper_authoring.py:300live

5. Async contracts

Consumes

33 registrations over 31 subjects. Streams are owned by the workers except AUTHORING_CORE, which this service creates (app/consumers/manager.py:42-55). Durable names are derived mechanically as asv2-<subject-with-dashes> (manager.py:68-71).

SubjectStreamDurablePublished byFeatureVerdict
authoring.asset.upload.completedAUTHORING_COREasv2-asset-upload-completedasset-manager app/api/v1/assets.py:52Uploaded document to RAG indexlive
authoring.chat.tool-call.requestedAUTHORING_COREasv2-chat-tool-call-requestednothing — the chat-worker that would publish it has no source in this repo (ARCHITECTURE.md §2 "Empty shells"; New-Design/SDD-authoring-chat-worker.md:63 calls it a future alternative to HTTP dispatch)Creation chatdead
authoring.content.generation.failedAUTHORING_CONTENTasv2-content-generation-failedcontent-worker app/workers/_base.py:83Document to modulelive
authoring.content.generation.progressAUTHORING_CONTENTasv2-content-generation-progresscontent-worker app/pipeline/orchestrator_v3.py:119Document to modulelive
authoring.content.generation.readyAUTHORING_CONTENTasv2-content-generation-readycontent-worker app/workers/content_worker.py:252Document to modulelive
authoring.content.regenerate.failedAUTHORING_CONTENTasv2-content-regenerate-failedcontent-worker app/workers/_base.py:130The builder (27 tools)live
authoring.content.regenerate.readyAUTHORING_CONTENTasv2-content-regenerate-readycontent-worker app/workers/content_worker.py:747The builder (27 tools)live
authoring.extract.gates.readyAUTHORING_EXTRACTasv2-extract-gates-readyextraction-worker app/consumers/run_consumer.py:145Extraction review gatelive
authoring.extract.progressAUTHORING_EXTRACTasv2-extract-progressextraction-worker app/consumers/run_consumer.py:38Document to modulelive
authoring.extract.run.failedAUTHORING_EXTRACTasv2-extract-run-failedextraction-worker app/consumers/run_consumer.py:50Document to modulelive
authoring.extract.tree.readyAUTHORING_EXTRACTasv2-extract-tree-readyextraction-worker app/consumers/structure_consumer.py:115Document to modulelive
authoring.image.batch.failedAUTHORINGasv2-image-batch-failedimage-worker app/worker.py:1233Illustrationslive
authoring.image.batch.readyAUTHORINGasv2-image-batch-readyimage-worker app/worker.py:1172Illustrationslive
authoring.image.job.failedAUTHORINGasv2-image-job-failedimage-worker app/worker.py:1234Illustrationslive
authoring.image.job.progressAUTHORINGasv2-image-job-progressimage-worker app/worker.py:1220Illustrationslive
authoring.image.job.readyAUTHORINGasv2-image-job-readyimage-worker app/worker.py:1173Illustrationslive
authoring.rag.context.failedAUTHORING_RAGasv2-rag-context-failedrag-context-worker app/publishers/events_publisher.py:63Document to modulelive
authoring.rag.context.readyAUTHORING_RAGasv2-rag-context-readyrag-context-worker app/publishers/events_publisher.py:60Document to modulelive
authoring.rag.indexing.failedAUTHORING_RAGasv2-rag-indexing-failedrag-context-worker app/publishers/events_publisher.py:57Document to modulelive
authoring.rag.indexing.progressAUTHORING_RAGasv2-rag-indexing-progressrag-context-worker app/publishers/events_publisher.py:40Document to modulelive
authoring.rag.indexing.readyAUTHORING_RAGasv2-rag-indexing-readyrag-context-worker app/publishers/events_publisher.py:54Document to modulelive
authoring.research.progressAUTHORING_RESEARCHasv2-research-progressresearch-worker app/consumers/deep_consumer.py:31Research to modulelive
authoring.research.run.failedAUTHORING_RESEARCHasv2-research-run-failedresearch-worker app/consumers/run_consumer.py:43Research to modulelive
authoring.research.topics.readyAUTHORING_RESEARCHasv2-research-topics-readyresearch-worker app/consumers/run_consumer.py:127Research to modulelive
authoring.research.tree.readyAUTHORING_RESEARCHasv2-research-tree-readyresearch-worker app/consumers/deep_consumer.py:174Research to modulelive
authoring.suggest.outline.failedAUTHORING_SUGGESTasv2-suggest-outline-failedsuggest-pipeline-worker app/workers/outline_worker.py:68Document to modulelive
authoring.suggest.outline.readyAUTHORING_SUGGESTasv2-suggest-outline-readysuggest-pipeline-worker app/workers/outline_worker.py:126Document to modulelive
authoring.suggest.suggestions.readyAUTHORING_SUGGESTasv2-suggest-suggestions-readysuggest-pipeline-worker app/workers/suggestions_worker.py:123Suggestionslive
authoring.tool-call.requestedscan artifact: the three JSON rows are the generic subscribe/pull_subscribe calls in app/consumers/manager.py:182,194,213 (loop variable subject). No handler registers it — app/consumers/register.py has no entry. The service only publishes it.dead
authoring.usage.eventAUTHORING_USAGEasv2-usage-eventcontent-worker app/workers/_base.py:192 · image-worker app/worker.py:1333 · users-auth app/onboarding_enrichment/processor.py:364 · self app/services/tool_dispatcher.py:256AI credit accountinglive
authoring.ws.broker.>core subscription, no queue group (every pod gets every frame)this service, one frame per WS publish — app/ws/broker.py:201Live updates (WebSocket)live

Publishes

SubjectConsumed byFeatureVerdict
authoring.extract.run.requestedextraction-worker app/main.py:35 (durable extraction-worker-run)Document to modulelive
authoring.extract.structure.requestedextraction-worker app/main.py:42Extraction review gatelive
authoring.rag.indexing.requestedrag-context-worker app/consumers/indexing_consumer.py:194Uploaded document to RAG indexlive
authoring.rag.context.requestedrag-context-worker app/consumers/retrieval_consumer.py:73Document to modulelive
authoring.suggest.outline.requestedsuggest-pipeline-worker app/workers/outline_worker.py:40Document to modulelive
authoring.content.generation.requestedcontent-worker app/workers/content_worker.py:93Document to modulelive
authoring.content.generation.requested.v3content-worker app/workers/content_worker.py:121Research to modulelive
authoring.content.translate.requestedcontent-worker app/workers/content_worker.py:143Publish and translatelive
authoring.image.batch.requestedimage-worker app/worker.py:392Illustrationslive
authoring.research.run.requestedresearch-worker app/main.py:35Research to modulelive
authoring.research.deep.requestedresearch-worker app/main.py:42Research to modulelive
authoring.tool-call.cancelledimage-worker app/worker.py:409Run controllive
authoring.usage.eventthis service — app/consumers/register.py:68AI credit accountinglive
training.notifications.ten.*.admin.*notification-worker app/core/config.py:115 (filter training.notifications.ten.*.admin.>, durable notification_worker_admin)Admin notificationslive
authoring.usage.reconciliation_driftnothing — the only authoring.usage.> consumer filters authoring.usage.event (app/consumers/register.py:68); no other service subscribes itAI credit accountingdead
authoring.tool-call.requestednothing — app/tools/scopes.py:8-9 states it outright, and no subscription exists in any serviceDocument to module (fallback path)dead

Three more subjects leave this service through a variable the scanner cannot resolve, so they are not in the JSON: the eight async catalog tools publish their own subjects via tool.nats_subject (app/services/tool_dispatcher.py:287, libs/oper-tools/oper_tools/catalog.json) — authoring.image.job.requested, authoring.content.regenerate.requested, authoring.suggest.suggestions.requested among them — and budget alerts publish authoring.usage.quota_warning / authoring.usage.quota_exceeded (app/services/budget_service.py:35-36,281), which nothing consumes (§8).

Background jobs

JobScheduleWhat it doesVerdict
ConsumerManager._pull_loopone task per JetStream subject, continuous Fetches, dedups, handles and acks worker events; parks poison messages on authoring.dlq.<tail> after 5 deliveries.live
_retention_loopevery 60s Deletes expired ws_replay_events and processed_events older than 7 days, off the event loop thread.live
_reconciliation_loopevery 86400s, 300s after boot Audits the usage ledger against OpenRouter, writes a reconciliation_runs row and publishes the drift event.live
_run_stall_sweep_loopevery 120s Parks extraction and research runs whose terminal event never arrived; sweeps abandoned creation chats and sends gate nudges.live
_settlement_sweep_loopevery 900s Refund backstop for debited runs that failed, plus the delayed true-up for completed ones.live
UsageService.bump_cachefire-and-forget per usage event Increments the per-tenant daily quota counter in Redis. Known defect: the task is unreferenced and can be collected before it runs (bug-hunt #10).live

All five loops are created in the lifespan (app/main.py:186-195) and cancelled on shutdown; _pull_loop tasks are created by consumer_manager.start() (app/main.py:185, app/consumers/manager.py:220). There is no CronJob — every job is in-process, and with replicas: 1 there is exactly one of each.

6. Data it owns

Postgres authoring_service_v2_db, 29 alembic revisions (migrations/versions/0001…0029). frameworks and module_framework_outlines were dropped in 0029_drop_frameworks; industry_taxonomy became category_synonyms and per-tenant categories were unified in 0028_tenant_categories_one_vocabulary.

TableWhat it holdsWritten by
modulesOne row per authoring-side module: title, category, difficulty, stage, status, root identity shared with micro-learning, provenance of copies.this service
draftsThe whole editable tree as one JSONB document (sections → lessons → screens → blocks).this service + content-worker (raw UPDATE drafts SET state, authoring-content-worker/app/pipeline/block_writer.py:66-106; ARCHITECTURE.md §3.4)
draft_versionsImmutable snapshot per mutation — what restore-draft-version reads.this service
tool_callsAudit log of every tool invocation: input, output, status, idempotency key, actor.this service
course_generation_runsOrchestrator state for a generation run: steps, phase timings, progress counters, cost estimate, terminal status.this service
processed_eventsEvent-id dedup ledger for the NATS consumers. Swept after 7 days.this service
ws_replay_eventsShort replay buffer so a reconnecting WebSocket client does not miss frames. Swept at 2×TTL.this service
usage_eventsPer-call AI cost ledger (tokens, cents, credits, model, status, attribution).this service
tenant_budgetsPer-tenant monthly cap and override.this service
reconciliation_runsResult of each nightly ledger-vs-OpenRouter audit, including drift percentage and bad samples.this service
marketplace_categoriesThe one classification axis: platform rows (office) plus per-tenant private rows.this service
marketplace_modulesThe public catalog — one snapshotted card per listed office module, no tenant_id by construction.this service
category_synonymsBilingual synonym table used to match free text to a category.this service
creation_chatsOne guided conversation that builds a module: mode, state, attached run, deadlines.this service
creation_chat_messagesThe cards in that conversation, in sequence.this service
research_runsResearch-mode run state: locale, phase, discovery/deep status.this service
research_topicsTopics discovered for a research run and the author’s confirmation.this service
rag_doc_refsLink from an uploaded asset to its RAG document id and parsed artifact key.this service
extraction_runsExtraction-engine run header: gates, coverage and fidelity scores.this service
extraction_segmentsParsed source-document segments.this service
extraction_salience_unitsCandidate units scored for importance.this service
extraction_pointsExtracted teachable points with evidence.this service
extraction_conceptsClustered concepts the content fan-out is keyed on.this service
extraction_sectionsProposed sections for the module.this service
extraction_lessonsProposed lessons, with descriptions.this service
extraction_review_itemsThe review queue a coordinator drains at the gate; every override records who and why.this service
extraction_ledgerPer-stage accounting for an extraction run.this service
extraction_reconciliation_opsRepair operations applied when extraction stages disagree.this service

Another service writes into this database

content-worker has no database of its own: it updates drafts.state with raw SQL (SELECT … FOR UPDATE) as it writes each lesson, so the draft document has two writers (ARCHITECTURE.md §3.4, authoring-content-worker/app/pipeline/block_writer.py:66-106). Any change to the draft JSONB shape is a two-service change.

7. Dependencies

flowchart LR
  UA["users-auth-service: admin proxy H22"] --> AV2
  ML["micro-learning: recent modules H14"] --> AV2
  WK["6 pipeline workers"] -.-> AV2
  AMIN["asset-manager: upload completed"] -.-> AV2
  AV2["authoring-service-v2"] --> MLO["micro-learning: ingest, publish, translate"]
  AV2 --> UAO["users-auth: tenant, profiles, credit debit"]
  AV2 --> SPW["suggest-pipeline-worker"]
  AV2 --> AM["asset-manager"]
  AV2 --> RAG["rag-context-worker"]
  AV2 --> STT["stt-service"]
  AV2 -.-> WK
  AV2 -.-> NW["notification-worker"]
  AV2 --> PG["Postgres authoring_service_v2_db"]
  AV2 --> RD["Redis db 9"]

Solid = HTTP, dotted = NATS. Outbound HTTP edges H1-H5 plus stt-service; inbound H14 and H22 (ARCHITECTURE.md §3.2).

8. Dead-code verdicts

Every entry point with no in-repo caller. Deleting is a separate decision — see the hub roll-up. Tests are not callers. The admin web app is not in this repo, so an admin-facing route with no in-repo caller would be suspect; in practice none are, because users-auth proxies the whole admin prefix.

Entry pointKindVerdictEvidence
GET /metricsHTTP routesuspectPrometheus format is rendered (app/main.py:268) but no scrape config exists in this repo — a grep for ServiceMonitor/PrometheusRule/prometheus.io/scrape across every *.yaml matches only users-auth-service/k8s/otp-worker-deployment.yaml. The alert rules in docs/ai-cost-monitoring.md:30-60 are a doc snippet, not an applied manifest. Resolved by looking at the cluster’s Prometheus config.
GET /internal/v1/pipeline/healthHTTP routedeadInternal-only surface (X-Oper-Key, app/routes/internal_routes.py:29-31). No in-repo caller: the only other paths users-auth’s back-office client fetches are /internal/v1/pipeline/stats, /internal/v1/runs, /internal/v1/runs/{run_id} (users-auth-service/app/clients/backoffice_clients.py:229,268,294). Named only in docs/API_ENDPOINTS.md:297.
GET /internal/v1/dbos/workflows, GET …/{workflow_id}, POST …/{workflow_id}/cancel, POST …/{workflow_id}/resumeHTTP routes (4)deadInternal-only DBOS operator surface (app/routes/internal_routes.py:442-482). Grepped the whole repo for dbos/workflows: only the handlers themselves and docs/API_ENDPOINTS.md:301-304. Nothing in any service, CronJob or script calls them.
POST /internal/v1/credits/reconcile, GET /internal/v1/credits/reconciliation-runs, GET /internal/v1/credits/tenants, PUT /internal/v1/credits/tenants/{tenant_id}/budgetHTTP routes (4)deadInternal-only (require_internal_key, app/routes/ai_usage_routes.py:56-60). Grep for credits/reconcile, reconciliation-runs, credits/tenants outside this service returns nothing but tests and the runbook (docs/ai-cost-monitoring.md:95-96). The nightly loop does the same work unattended (app/main.py:187), so nothing needs to call them.
GET/POST /internal/v1/marketplace/category-synonyms, PATCH …/{synonym_id}HTTP routes (3)deadInternal-only (app/routes/marketplace_routes.py:595-598). No in-repo caller; the admin surface uses /v1/admin/authoring/categories instead, and the synonym table is only read internally by the matcher. Named only in docs/marketplace-plan.md:168 and docs/API_ENDPOINTS.md:402.
authoring.chat.tool-call.requestedNATS consumer (asv2-chat-tool-call-requested, AUTHORING_CORE)deadRegistered at app/consumers/register.py:62, handler app/consumers/chat_consumer.py. No publisher exists: the chat-worker that would emit it has no tracked source (ARCHITECTURE.md §2, “Empty shells”), New-Design/SDD-authoring-chat-worker.md:63 lists the subject as a future alternative to HTTP dispatch, and authoring-service-v2/TODO.md:187 (E23) still describes the publisher as work to do. Today the creation chat dispatches tools in-process.
authoring.tool-call.requestedNATS publishdeadPublished on the non-DBOS fallback path (app/workflows/doc_to_module.py:187, kicking index-document) and as the dispatcher’s last-resort subject (app/services/tool_dispatcher.py:287 — unreachable, every async catalog entry carries its own nats_subject). Nothing subscribes to it anywhere; app/tools/scopes.py:8-9 says so in the source. Consequence: if DBOS is unavailable, a doc run’s first step is published where rag-context-worker is not listening (it filters authoring.rag.indexing.requested, rag-context-worker/app/consumers/indexing_consumer.py:194) and the run never advances.
authoring.usage.reconciliation_driftNATS publishdeadPublished at app/services/reconciliation_service.py:202. The only consumer of authoring.usage.> is this service’s usage consumer, filtered to authoring.usage.event (app/consumers/register.py:68); ARCHITECTURE.md §3.3 confirms AUTHORING_USAGE is consumed only by that durable. Drift is observed through the metric and the audit table instead.
authoring.usage.quota_warning, authoring.usage.quota_exceededNATS publishes (not in the scan — variable subject)deadConstants at app/services/budget_service.py:35-36, published through a subject parameter at :281, which is why the scanner attributes those lines to another subject. No consumer anywhere in the repo. Until the Redis host was corrected these alerts could not even be emitted (the once-per-month dedup flag write failed first) — see k8s/configmap.yaml:11-19.
partial run statusUnreachable statedeadapp/repositories/run_repository.py:405,417 and app/routes/internal_routes.py:36 all handle it, but no code path ever sets it — bug-hunt report finding #12 part 2 (deferred) is the plan to make it reachable.
Cross-pod WebSocket fan-outNATS consumer pathliveauthoring.ws.broker.> is registered without a queue group (app/consumers/register.py:73) and published per frame (app/ws/broker.py:201), so it works — but the Deployment runs replicas: 1 (k8s/deployment.yaml:9), so today the only receiver is the pod that published. Live code, currently a no-op in production.

9. Sources