In one sentence
Authoring and learning are two different services with two different databases. Publishing is the moment a draft crosses from one to the other: authoring-v2 pushes the finished module into micro-learning over HTTP, and from then on the learner-facing copy is the one that matters.
This is the only place in the platform where a user-visible action depends on a synchronous call between two services — if micro-learning is down, publishing fails immediately rather than queueing.
Publishing
sequenceDiagram autonumber participant A as Author participant AV2 as authoring-v2 participant UA as users-auth participant ML as micro-learning A->>AV2: publish-module tool AV2->>UA: debit credits AV2->>ML: POST /v1/internal/modules/ingest ML-->>AV2: module id + version Note over ML: module visible to learners ML-->>ML: tts.audio.requested per screen
- The author runs the
publish-moduletool. authoring-v2 validates the draft, then calls micro-learning's ingest endpoint — authoring-service-v2/app/core/http.py:56. - micro-learning creates the module, its sections, lessons, screens and blocks, and returns the new identifiers. Publishing is versioned: republishing produces a new version rather than mutating the live one, and learners mid-module are not disturbed.
- Unpublish and delete follow the same shape, addressed by root id rather than module id — http.py:197, :217, :235 — because a module's identity survives across versions.
- Credits are debited at publish as well as at run start; see credits & billing.
Translation
Translation is retrofitted onto an already-published module rather than being part of the first publish. authoring-v2 publishes authoring.content.translate.requested (app/services/publish_service.py:376 and app/tools/handlers/translate_handlers.py:97); content-worker consumes it, translates the content, and writes the result back to micro-learning directly over HTTP rather than routing it through authoring-v2 — authoring-content-worker/app/clients/ml_client.py. It is the one place a pipeline worker talks to a service other than authoring-v2 and asset-manager.
Narration audio
sequenceDiagram autonumber participant ML as micro-learning participant TTS as tts worker participant MOD as Modal chatterbox participant AM as asset-manager ML-->>TTS: tts.audio.requested TTS->>MOD: synthesise speech MOD-->>TTS: audio TTS->>AM: upload file TTS->>ML: write lesson_screen_audio row Note over TTS,ML: same database, no completion event
Two things ask for audio: publishing a module can pre-warm narration for every screen (micro-learning-service-v2/app/services/screen_audio_service.py:108), and a reconciler sweeps for screens whose audio is missing or stale (app/services/audio_reconciler_scheduler.py:127). Both publish tts.audio.requested.
The TTS worker is a separate Deployment with a separate image — it carries ffmpeg and a torch-sized dependency set — but it is not a separate service: its code lives in micro-learning-service-v2/app/tts_worker/ and it writes straight into micro-learning's lesson_screen_audio table with SELECT … FOR UPDATE SKIP LOCKED. There is no completion event; the row changing state is the completion signal. That is the tightest coupling on the platform and the reason the two were merged into one codebase.
Where it breaks today
| Issue | Detail |
|---|---|
| Publishing is a blocking cross-service call | If micro-learning is unavailable, publish fails in the author's face. There is no retry queue. Deliberate — a half-published module would be worse — but worth knowing. |
| Audio has no failure signal | Because completion is a database row rather than an event, a synthesis failure surfaces only through the reconciler's next sweep. See micro-learning async contracts. |
| Three audio operations are unreachable | /v1/internal/audio/reconcile, /retry-failed and /prewarm/{module_id} have no in-repo caller and no ingress rule — the work runs on in-process timers instead. See the micro-learning verdicts. |
| Publish events are published but unread | training.module.published is mapped by feed-service, but feed's event processor is never started, so nothing consumes it. See feed-service. |