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 it | Admin 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 |
| Runtime | FastAPI + WebSocket + 30 NATS subscriptions + DBOS durable workflows ·
Deployment authoring-service-v2, replicas: 1
(k8s/deployment.yaml:4,9) |
| Database | Postgres 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) |
| Redis | users-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 streams | Reads 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 APIs | None 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 points | 67 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
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
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
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_idin 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
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
| Tool | Group | What it achieves | Registered at |
|---|---|---|---|
create-module | Module shelf | Start a new module and its empty draft. | app/tools/register.py:26 |
update-module | Module shelf | Rename it, change description, category, difficulty, goal, skills, cover image. Allow-listed fields only (bug-hunt #11). | app/tools/register.py:27 |
duplicate-module | Module shelf | Copy a module and its whole tree into a new draft. | app/tools/register.py:28 |
archive-module | Catalog state | Take it off the shelf without deleting it. | app/tools/register.py:32 |
unarchive-module | Catalog state | Put an archived module back. | app/tools/register.py:33 |
unpublish-module | Catalog state | Pull the published version out of micro-learning. | app/tools/register.py:34 |
delete-module | Catalog state | Destroy the module and its versions. Irreversible. | app/tools/register.py:35 |
create-section | Tree edit | Add a section to the draft. | app/tools/register.py:38 |
update-section | Tree edit | Retitle or re-describe a section. | app/tools/register.py:39 |
delete-section | Tree edit | Remove a section and everything under it. | app/tools/register.py:40 |
create-lesson | Tree edit | Add a lesson to a section. | app/tools/register.py:41 |
update-lesson | Tree edit | Edit lesson title, duration, type, requirements. | app/tools/register.py:42 |
delete-lesson | Tree edit | Remove a lesson. | app/tools/register.py:43 |
create-screen | Tree edit | Add a screen to a lesson. | app/tools/register.py:44 |
delete-screen | Tree edit | Remove a screen. | app/tools/register.py:45 |
create-block | Tree edit | Add a content block (text, quiz, divider, image, …) to a screen. | app/tools/register.py:46 |
update-block | Tree edit | Edit a block in place. | app/tools/register.py:47 |
delete-block | Tree edit | Remove a block. | app/tools/register.py:48 |
move-lesson | Tree edit | Move a lesson to another section. | app/tools/register.py:49 |
reorder | Tree edit | Reorder sections, lessons, screens or blocks. | app/tools/register.py:50 |
restore-draft-version | Versioning | Roll the draft back to an earlier snapshot. | app/tools/register.py:53 |
publish-module | Publishing | Send the draft to micro-learning as a new version and make it live. | app/tools/register.py:56 |
translate-module | Publishing | Retrofit a translation of an already-published module. | app/tools/register.py:58 |
upload-document | Source documents | Get an upload URL from asset-manager and register the source file. | app/tools/register.py:59 |
request-module-from-doc | Generation run | Start the document→module run. | app/tools/register.py:63 |
request-module-from-prompt | Generation run | Start the research→module run from a prompt. | app/tools/register.py:66 |
cancel-run | Run control | Stop a run and tell the workers to drop pending work. | app/tools/register.py:70 |
Drafts and version history live
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}, toolrestore-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
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
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-documentandrequest-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
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; publishesauthoring.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
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
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
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
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); publishesauthoring.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
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
totalstill counts them (bug-hunt #14, deferred)
Marketplace — office curation live
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-managerPOST /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
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
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
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
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
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, thedbosschema; publishesauthoring.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
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
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), consumerauthoring.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
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
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→ publishauthoring.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
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
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.
| Method | Path | Auth | Feature | Callers | Verdict |
|---|---|---|---|---|---|
| GET | /health/live | none (unauthenticated) | Health probes | k8s livenessProbe k8s/deployment.yaml:68 | live |
| GET | /health/ready | none (unauthenticated) | Health probes | k8s readinessProbe k8s/deployment.yaml:75 | live |
| POST | /internal/v1/credits/reconcile | X-Oper-Key (internal) | Ops surfaces | none in repo — runbook curl only, docs/ai-cost-monitoring.md:95 | dead |
| GET | /internal/v1/credits/reconciliation-runs | X-Oper-Key (internal) | Ops surfaces | none in repo — runbook curl only, docs/ai-cost-monitoring.md:95 | dead |
| GET | /internal/v1/credits/tenants | X-Oper-Key (internal) | Ops surfaces | none in repo — runbook curl only, docs/ai-cost-monitoring.md:95 | dead |
| PUT | /internal/v1/credits/tenants/{tenant_id}/budget | X-Oper-Key (internal) | Ops surfaces | none in repo — runbook curl only, docs/ai-cost-monitoring.md:95 | dead |
| GET | /internal/v1/dbos/workflows | X-Oper-Key (internal) | Ops surfaces | none in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304 | dead |
| GET | /internal/v1/dbos/workflows/{workflow_id} | X-Oper-Key (internal) | Ops surfaces | none in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304 | dead |
| POST | /internal/v1/dbos/workflows/{workflow_id}/cancel | X-Oper-Key (internal) | Ops surfaces | none in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304 | dead |
| POST | /internal/v1/dbos/workflows/{workflow_id}/resume | X-Oper-Key (internal) | Ops surfaces | none in repo — documented as an ops surface, docs/API_ENDPOINTS.md:301-304 | dead |
| GET | /internal/v1/marketplace/category-synonyms | X-Oper-Key (internal) | Ops surfaces | none in repo — docs/marketplace-plan.md:168 | dead |
| POST | /internal/v1/marketplace/category-synonyms | X-Oper-Key (internal) | Ops surfaces | none in repo — docs/marketplace-plan.md:168 | dead |
| PATCH | /internal/v1/marketplace/category-synonyms/{synonym_id} | X-Oper-Key (internal) | Ops surfaces | none in repo — docs/marketplace-plan.md:168 | dead |
| GET | /internal/v1/pipeline/health | X-Oper-Key (internal) | Ops surfaces | none in repo — docs/API_ENDPOINTS.md:297 | dead |
| GET | /internal/v1/pipeline/stats | X-Oper-Key (internal) | Back-office run inbox | users-auth app/clients/backoffice_clients.py:229 | live |
| GET | /internal/v1/runs | X-Oper-Key (internal) | Back-office run inbox | users-auth backoffice_clients.py:268 | live |
| GET | /internal/v1/runs/{run_id} | X-Oper-Key (internal) | Back-office run inbox | users-auth backoffice_clients.py:294 | live |
| GET | /internal/v1/tenants/{tenant_id}/modules/recent | X-Oper-Key (internal) | Recent-modules card | micro-learning app/clients/authoring_client.py:32 (H14) | live |
| GET | /metrics | none (unauthenticated) | Prometheus metrics | none in repo — no ServiceMonitor / scrape annotation anywhere | suspect |
| GET | /v1/admin/authoring/assets/{asset_id}/import-estimate | admin JWT · admin.panel | Cost estimates | users-auth proxy oper_authoring.py:369 · docs/fe-import-estimate.md:12 | live |
| GET | /v1/admin/authoring/categories | admin JWT · admin.panel | Categories | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:373 | live |
| POST | /v1/admin/authoring/categories | admin JWT · admin.panel | Categories | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:373 | live |
| PATCH | /v1/admin/authoring/categories/{category_id} | admin JWT · admin.panel (+ office for platform rows) | Categories | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:375 | live |
| GET | /v1/admin/authoring/course-generation-runs | admin JWT · admin.panel | Document to module | users-auth proxy oper_authoring.py:369 · docs/frontend-integration.md:437 | live |
| GET | /v1/admin/authoring/course-generation-runs/{run_id} | admin JWT · admin.panel | Document to module | users-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:23 | live |
| GET | /v1/admin/authoring/course-generation-runs/{run_id}/extraction | admin JWT · admin.panel | Extraction review gate | users-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150 | live |
| POST | /v1/admin/authoring/course-generation-runs/{run_id}/extraction/proceed | admin JWT · content.write | Extraction review gate | users-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150 | live |
| GET | /v1/admin/authoring/course-generation-runs/{run_id}/extraction/review-items | admin JWT · admin.panel | Extraction review gate | users-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150 | live |
| POST | /v1/admin/authoring/course-generation-runs/{run_id}/extraction/review-items/{item_id} | admin JWT · content.write | Extraction review gate | users-auth proxy oper_authoring.py:369 · docs/fe-progress-integration.md:150 | live |
| GET | /v1/admin/authoring/course-generation-runs/{run_id}/research | admin JWT · admin.panel | Research to module | users-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:142 | live |
| GET | /v1/admin/authoring/course-generation-runs/{run_id}/research/outline | admin JWT · admin.panel | Research to module | users-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:122 | live |
| GET | /v1/admin/authoring/course-generation-runs/{run_id}/research/topics | admin JWT · admin.panel | Research to module | users-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:92 | live |
| POST | /v1/admin/authoring/course-generation-runs/{run_id}/research/topics/confirm | admin JWT · content.write | Research to module | users-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:103 | live |
| GET | /v1/admin/authoring/creation-chats | admin JWT · admin.panel | Creation chat | users-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:226 | live |
| POST | /v1/admin/authoring/creation-chats | admin JWT · content.write | Creation chat | users-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:49 | live |
| GET | /v1/admin/authoring/creation-chats/{chat_id} | admin JWT · admin.panel | Creation chat | users-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:230 | live |
| POST | /v1/admin/authoring/creation-chats/{chat_id}/messages | admin JWT · content.write | Creation chat | users-auth proxy oper_authoring.py:369 · docs/fe-creation-chat.md:175 | live |
| GET | /v1/admin/authoring/credits/budget | admin JWT · admin.panel | AI credits and budget | users-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:172 | live |
| GET | /v1/admin/authoring/credits/events | admin JWT · admin.panel | AI credits and budget | users-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:171 | live |
| GET | /v1/admin/authoring/credits/runs/{run_id} | admin JWT · admin.panel | AI credits and budget | users-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:170 | live |
| GET | /v1/admin/authoring/credits/summary | admin JWT · admin.panel | AI credits and budget | users-auth proxy oper_authoring.py:369 · docs/ai-cost-usage-plan.md:169 | live |
| GET | /v1/admin/authoring/drafts/by-module/{module_id} | admin JWT · admin.panel | Drafts and versions | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:201 | live |
| GET | /v1/admin/authoring/drafts/{draft_id} | admin JWT · admin.panel | Drafts and versions | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:200 | live |
| GET | /v1/admin/authoring/drafts/{draft_id}/versions | admin JWT · admin.panel | Drafts and versions | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:202 | live |
| GET | /v1/admin/authoring/drafts/{draft_id}/versions/{version_no} | admin JWT · admin.panel | Drafts and versions | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:203 | live |
| GET | /v1/admin/authoring/marketplace/listings | admin JWT · office tenant only | Marketplace curation | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:194 | live |
| GET | /v1/admin/authoring/marketplace/listings/{catalog_id} | admin JWT · office tenant only | Marketplace curation | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:194 | live |
| PATCH | /v1/admin/authoring/marketplace/listings/{catalog_id} | admin JWT · content.publish · office tenant only | Marketplace curation | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:195 | live |
| GET | /v1/admin/authoring/marketplace/modules | admin JWT · admin.panel | Marketplace browse and copy | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:60 | live |
| GET | /v1/admin/authoring/marketplace/modules/{catalog_id} | admin JWT · admin.panel | Marketplace browse and copy | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:233 | live |
| POST | /v1/admin/authoring/marketplace/modules/{catalog_id}/copy | admin JWT · content.write | Marketplace browse and copy | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:235 | live |
| GET | /v1/admin/authoring/marketplace/modules/{catalog_id}/preview | admin JWT · admin.panel | Marketplace browse and copy | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:234 | live |
| GET | /v1/admin/authoring/modules | admin JWT · admin.panel | Module shelf | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:162 | live |
| GET | /v1/admin/authoring/modules/{module_id} | admin JWT · admin.panel | Module shelf | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:162 | live |
| GET | /v1/admin/authoring/modules/{module_id}/narration-estimate | admin JWT · admin.panel | Publish and translate | users-auth proxy oper_authoring.py:369 · docs/module-translations-fe.md:157 | live |
| GET | /v1/admin/authoring/modules/{module_id}/runs | admin JWT · admin.panel | Module shelf | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:163 | live |
| POST | /v1/admin/authoring/prompt-import-context | admin JWT · content.write | Cost estimates | users-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:34 | live |
| GET | /v1/admin/authoring/prompt-import-estimate | admin JWT · admin.panel | Cost estimates | users-auth proxy oper_authoring.py:369 · docs/fe-research-module.md:20 | live |
| GET | /v1/admin/authoring/tenants/{source_tenant_id}/modules | admin JWT · office tenant only | Marketplace curation | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:196 | live |
| POST | /v1/admin/authoring/tenants/{source_tenant_id}/modules/{source_module_id}/copy | admin JWT · content.write · office tenant only | Marketplace curation | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:198 | live |
| GET | /v1/admin/authoring/tenants/{source_tenant_id}/modules/{source_module_id}/preview | admin JWT · office tenant only | Marketplace curation | users-auth proxy oper_authoring.py:369 · docs/marketplace-plan.md:198 | live |
| GET | /v1/admin/authoring/tool-calls | admin JWT · admin.panel | The builder (27 tools) | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:85 | live |
| GET | /v1/admin/authoring/tool-calls/{tool_call_id} | admin JWT · admin.panel | The builder (27 tools) | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:94 | live |
| GET | /v1/admin/authoring/tools | admin JWT · admin.panel | The builder (27 tools) | users-auth proxy oper_authoring.py:369 · docs/API_ENDPOINTS.md:52 | live |
| POST | /v1/admin/authoring/tools/{tool_name} | admin JWT · per-tool scope | The builder (27 tools) | users-auth proxy oper_authoring.py:369 · docs/frontend-integration.md:108 | live |
| POST | /v1/admin/authoring/transcriptions | admin JWT · content.write | Voice dictation | users-auth proxy oper_authoring.py:369 · docs/fe-voice-dictation.md:13 | live |
| WS | /v1/admin/authoring/ws | admin JWT in query string | Live updates (WebSocket) | console browser direct, token from users-auth oper_authoring.py:300 | live |
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).
| Subject | Stream | Durable | Published by | Feature | Verdict |
|---|---|---|---|---|---|
authoring.asset.upload.completed | AUTHORING_CORE | asv2-asset-upload-completed | asset-manager app/api/v1/assets.py:52 | Uploaded document to RAG index | live |
authoring.chat.tool-call.requested | AUTHORING_CORE | asv2-chat-tool-call-requested | nothing — 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 chat | dead |
authoring.content.generation.failed | AUTHORING_CONTENT | asv2-content-generation-failed | content-worker app/workers/_base.py:83 | Document to module | live |
authoring.content.generation.progress | AUTHORING_CONTENT | asv2-content-generation-progress | content-worker app/pipeline/orchestrator_v3.py:119 | Document to module | live |
authoring.content.generation.ready | AUTHORING_CONTENT | asv2-content-generation-ready | content-worker app/workers/content_worker.py:252 | Document to module | live |
authoring.content.regenerate.failed | AUTHORING_CONTENT | asv2-content-regenerate-failed | content-worker app/workers/_base.py:130 | The builder (27 tools) | live |
authoring.content.regenerate.ready | AUTHORING_CONTENT | asv2-content-regenerate-ready | content-worker app/workers/content_worker.py:747 | The builder (27 tools) | live |
authoring.extract.gates.ready | AUTHORING_EXTRACT | asv2-extract-gates-ready | extraction-worker app/consumers/run_consumer.py:145 | Extraction review gate | live |
authoring.extract.progress | AUTHORING_EXTRACT | asv2-extract-progress | extraction-worker app/consumers/run_consumer.py:38 | Document to module | live |
authoring.extract.run.failed | AUTHORING_EXTRACT | asv2-extract-run-failed | extraction-worker app/consumers/run_consumer.py:50 | Document to module | live |
authoring.extract.tree.ready | AUTHORING_EXTRACT | asv2-extract-tree-ready | extraction-worker app/consumers/structure_consumer.py:115 | Document to module | live |
authoring.image.batch.failed | AUTHORING | asv2-image-batch-failed | image-worker app/worker.py:1233 | Illustrations | live |
authoring.image.batch.ready | AUTHORING | asv2-image-batch-ready | image-worker app/worker.py:1172 | Illustrations | live |
authoring.image.job.failed | AUTHORING | asv2-image-job-failed | image-worker app/worker.py:1234 | Illustrations | live |
authoring.image.job.progress | AUTHORING | asv2-image-job-progress | image-worker app/worker.py:1220 | Illustrations | live |
authoring.image.job.ready | AUTHORING | asv2-image-job-ready | image-worker app/worker.py:1173 | Illustrations | live |
authoring.rag.context.failed | AUTHORING_RAG | asv2-rag-context-failed | rag-context-worker app/publishers/events_publisher.py:63 | Document to module | live |
authoring.rag.context.ready | AUTHORING_RAG | asv2-rag-context-ready | rag-context-worker app/publishers/events_publisher.py:60 | Document to module | live |
authoring.rag.indexing.failed | AUTHORING_RAG | asv2-rag-indexing-failed | rag-context-worker app/publishers/events_publisher.py:57 | Document to module | live |
authoring.rag.indexing.progress | AUTHORING_RAG | asv2-rag-indexing-progress | rag-context-worker app/publishers/events_publisher.py:40 | Document to module | live |
authoring.rag.indexing.ready | AUTHORING_RAG | asv2-rag-indexing-ready | rag-context-worker app/publishers/events_publisher.py:54 | Document to module | live |
authoring.research.progress | AUTHORING_RESEARCH | asv2-research-progress | research-worker app/consumers/deep_consumer.py:31 | Research to module | live |
authoring.research.run.failed | AUTHORING_RESEARCH | asv2-research-run-failed | research-worker app/consumers/run_consumer.py:43 | Research to module | live |
authoring.research.topics.ready | AUTHORING_RESEARCH | asv2-research-topics-ready | research-worker app/consumers/run_consumer.py:127 | Research to module | live |
authoring.research.tree.ready | AUTHORING_RESEARCH | asv2-research-tree-ready | research-worker app/consumers/deep_consumer.py:174 | Research to module | live |
authoring.suggest.outline.failed | AUTHORING_SUGGEST | asv2-suggest-outline-failed | suggest-pipeline-worker app/workers/outline_worker.py:68 | Document to module | live |
authoring.suggest.outline.ready | AUTHORING_SUGGEST | asv2-suggest-outline-ready | suggest-pipeline-worker app/workers/outline_worker.py:126 | Document to module | live |
authoring.suggest.suggestions.ready | AUTHORING_SUGGEST | asv2-suggest-suggestions-ready | suggest-pipeline-worker app/workers/suggestions_worker.py:123 | Suggestions | live |
authoring.tool-call.requested | — | — | scan 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.event | AUTHORING_USAGE | asv2-usage-event | content-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:256 | AI credit accounting | live |
authoring.ws.broker.> | — | core subscription, no queue group (every pod gets every frame) | this service, one frame per WS publish — app/ws/broker.py:201 | Live updates (WebSocket) | live |
Publishes
| Subject | Consumed by | Feature | Verdict |
|---|---|---|---|
authoring.extract.run.requested | extraction-worker app/main.py:35 (durable extraction-worker-run) | Document to module | live |
authoring.extract.structure.requested | extraction-worker app/main.py:42 | Extraction review gate | live |
authoring.rag.indexing.requested | rag-context-worker app/consumers/indexing_consumer.py:194 | Uploaded document to RAG index | live |
authoring.rag.context.requested | rag-context-worker app/consumers/retrieval_consumer.py:73 | Document to module | live |
authoring.suggest.outline.requested | suggest-pipeline-worker app/workers/outline_worker.py:40 | Document to module | live |
authoring.content.generation.requested | content-worker app/workers/content_worker.py:93 | Document to module | live |
authoring.content.generation.requested.v3 | content-worker app/workers/content_worker.py:121 | Research to module | live |
authoring.content.translate.requested | content-worker app/workers/content_worker.py:143 | Publish and translate | live |
authoring.image.batch.requested | image-worker app/worker.py:392 | Illustrations | live |
authoring.research.run.requested | research-worker app/main.py:35 | Research to module | live |
authoring.research.deep.requested | research-worker app/main.py:42 | Research to module | live |
authoring.tool-call.cancelled | image-worker app/worker.py:409 | Run control | live |
authoring.usage.event | this service — app/consumers/register.py:68 | AI credit accounting | live |
training.notifications.ten.*.admin.* | notification-worker app/core/config.py:115 (filter training.notifications.ten.*.admin.>, durable notification_worker_admin) | Admin notifications | live |
authoring.usage.reconciliation_drift | nothing — the only authoring.usage.> consumer filters authoring.usage.event (app/consumers/register.py:68); no other service subscribes it | AI credit accounting | dead |
authoring.tool-call.requested | nothing — app/tools/scopes.py:8-9 states it outright, and no subscription exists in any service | Document 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
| Job | Schedule | What it does | Verdict |
|---|---|---|---|
ConsumerManager._pull_loop | one task per JetStream subject, continuous | Fetches, dedups, handles and acks worker events; parks poison messages on
authoring.dlq.<tail> after 5 deliveries. | live |
_retention_loop | every 60s | Deletes expired ws_replay_events and processed_events older than 7 days,
off the event loop thread. | live |
_reconciliation_loop | every 86400s, 300s after boot | Audits the usage ledger against OpenRouter, writes a reconciliation_runs row and
publishes the drift event. | live |
_run_stall_sweep_loop | every 120s | Parks extraction and research runs whose terminal event never arrived; sweeps abandoned creation chats and sends gate nudges. | live |
_settlement_sweep_loop | every 900s | Refund backstop for debited runs that failed, plus the delayed true-up for completed ones. | live |
UsageService.bump_cache | fire-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.
| Table | What it holds | Written by |
|---|---|---|
modules | One row per authoring-side module: title, category, difficulty, stage, status, root identity shared with micro-learning, provenance of copies. | this service |
drafts | The 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_versions | Immutable snapshot per mutation — what restore-draft-version reads. | this service |
tool_calls | Audit log of every tool invocation: input, output, status, idempotency key, actor. | this service |
course_generation_runs | Orchestrator state for a generation run: steps, phase timings, progress counters, cost estimate, terminal status. | this service |
processed_events | Event-id dedup ledger for the NATS consumers. Swept after 7 days. | this service |
ws_replay_events | Short replay buffer so a reconnecting WebSocket client does not miss frames. Swept at 2×TTL. | this service |
usage_events | Per-call AI cost ledger (tokens, cents, credits, model, status, attribution). | this service |
tenant_budgets | Per-tenant monthly cap and override. | this service |
reconciliation_runs | Result of each nightly ledger-vs-OpenRouter audit, including drift percentage and bad samples. | this service |
marketplace_categories | The one classification axis: platform rows (office) plus per-tenant private rows. | this service |
marketplace_modules | The public catalog — one snapshotted card per listed office module, no tenant_id by construction. | this service |
category_synonyms | Bilingual synonym table used to match free text to a category. | this service |
creation_chats | One guided conversation that builds a module: mode, state, attached run, deadlines. | this service |
creation_chat_messages | The cards in that conversation, in sequence. | this service |
research_runs | Research-mode run state: locale, phase, discovery/deep status. | this service |
research_topics | Topics discovered for a research run and the author’s confirmation. | this service |
rag_doc_refs | Link from an uploaded asset to its RAG document id and parsed artifact key. | this service |
extraction_runs | Extraction-engine run header: gates, coverage and fidelity scores. | this service |
extraction_segments | Parsed source-document segments. | this service |
extraction_salience_units | Candidate units scored for importance. | this service |
extraction_points | Extracted teachable points with evidence. | this service |
extraction_concepts | Clustered concepts the content fan-out is keyed on. | this service |
extraction_sections | Proposed sections for the module. | this service |
extraction_lessons | Proposed lessons, with descriptions. | this service |
extraction_review_items | The review queue a coordinator drains at the gate; every override records who and why. | this service |
extraction_ledger | Per-stage accounting for an extraction run. | this service |
extraction_reconciliation_ops | Repair 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 point | Kind | Verdict | Evidence |
|---|---|---|---|
GET /metrics | HTTP route | suspect | Prometheus 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/health | HTTP route | dead | Internal-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}/resume | HTTP routes (4) | dead | Internal-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}/budget | HTTP routes (4) | dead | Internal-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) | dead | Internal-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.requested | NATS consumer (asv2-chat-tool-call-requested, AUTHORING_CORE) | dead | Registered 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.requested | NATS publish | dead | Published 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_drift | NATS publish | dead | Published 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_exceeded | NATS publishes (not in the scan — variable subject) | dead | Constants 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 status | Unreachable state | dead | app/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-out | NATS consumer path | live | authoring.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
tools/feature-docs/out/authoring-service-v2.json— the scanned entry points this page is checked against.ARCHITECTURE.md§2 (inventory), §3.1 (hub shape), §3.2 (HTTP edges H1-H5, H14, H22 and the/internal/v1prefix trap), §3.3 (streams), §3.4 (shared database writers), §3.5 and §4.1(6) (Redis DNS), §4.2 (broken CI referencing the retired v1 Deployment).bug-hunt-reports/authoring-service-v2.md— 14 findings; #1, #2, #6, #7, #8, #11, #12 fixed, #3, #4, #5, #9, #10, #13, #14 deferred.authoring-service-v2/docs/API_ENDPOINTS.md— route-by-route reference, incl. §8 internal ops API and §10 categories.authoring-service-v2/docs/frontend-integration.md— tool dispatcher contract, read endpoints, WebSocket topics.authoring-service-v2/docs/worker-integration.md— the subject-by-subject worker contract.authoring-service-v2/docs/fe-creation-chat.md,fe-research-module.md,fe-import-estimate.md,fe-progress-integration.md,fe-voice-dictation.md,fe-module-lifecycle.md,fe-image-status.md,fe-divider-block.md,fe-quiz-select-word-block.md,module-translations-fe.md,create-draft-module.md,frontend-authoring-flow.md— frontend integration guides; each names the paths its screen calls.authoring-service-v2/docs/marketplace-plan.md— catalogue, office curation, copy in/out.authoring-service-v2/docs/ai-cost-usage-plan.mdandai-cost-monitoring.md— usage ledger, budgets, reconciliation, alert runbook.authoring-service-v2/docs/SDD-OPER-course-extraction-updated.mdand its-ADDENDUM-language/-ADDENDUM-integration— the extraction engine and its review gate.New-Design/SDD-composer-engine.md,New-Design/SDD-creation-chat.md,New-Design/PLAN-doc-creation-chat.md,New-Design/SDD-authoring-service.md,New-Design/SDD-authoring-chat-worker.md,New-Design/SDD-multi-admin.md,New-Design/SDD-admin-notification-centre.md— product naming and the tool/scope model.authoring-service-v2/README.md,READINESS.md,LIVE_TEST_GAPS.md,TODO.md(E23 — the chat-worker publisher that does not exist yet).