Course Delivery & Retakes (LMS)
No code, proto, schema, or service for this exists anywhere in the codebase today. This is a design spec for review, not a build in progress — nothing described here should be treated as implemented or scheduled until it is picked up as a backlog item in CLAUDE.md’s Roadmap Context.
The problem this closes
Custodian Workforce Compliance already tracks that a
credential lapsed and creates a DUE re-performance obligation on the
Schedule tab. But today a schedule
closes only one way: a custodian re-attests in person, after the fact, with nothing in between. For
the slice of credentials that are genuinely online-deliverable — refresher assessments, onboarding
modules, compliance quizzes — the holder has no in-app way to actually do the thing the schedule
is asking for. Not every credential is like this: a great deal of real-world credentialing has an
irreducible practical component (a site visit, a proctor, a physical skills test), and this design
must never let an online pass silently stand in for that.
Design principle: two paths, one completion contract
Institutions and custodians arrive with wildly different training stacks — some run Moodle, Cornerstone, or an in-house system; some have nothing. Forcing everyone onto one path either locks out custodians with an existing LMS investment, or leaves custodians with nothing unable to offer online retakes at all. So this is deliberately dual-path, converging on one shared contract:
- Path A — BYO-LMS bridge: the custodian/issuer keeps their existing LMS; Attest only receives a signed completion signal.
- Path B — Attest-native: Attest hosts a minimal authoring + delivery engine for custodians/issuers with no LMS of their own.
- CompletionEvent is the one contract every downstream consumer reacts to, regardless of which path produced it — this is what keeps the design modular instead of forking logic per protocol.
Where this fits in the architecture
Per Key Architectural Decision #1 (Core Engine is the only service that knows multi-service workflows) and the single-responsibility principle, this is a new microservice, not an extension of an existing one:
| Candidate home | Why it’s the wrong fit |
|---|---|
| Learner Records | Deliberately narrow and AVETMISS-shaped (Learner/Enrolment/Outcome for regulatory export). Records that training happened; was never meant to deliver it. |
| Custodian Registry | Owns DID/key custody for custodians only — no business-data storage responsibility. |
| Core Engine | Orchestrates; explicitly must not own service-specific business storage. |
Proposed: Learning & Course Delivery Service, :50057 (next open port after Custodian
Registry’s :50056), own PostgreSQL schema, own gRPC contract — the 9th proto in
backends/shared/src/main/proto/, following the existing
Modify a gRPC Contract workflow. Core Engine orchestrates it the
same way it orchestrates every other service today; it never talks to Credential Issuance or the
custodian schedule directly.
Core data model
| Entity | Scope | Key fields |
|---|---|---|
CourseDefinition | Both paths | owner_id (issuer or custodian), delivery_mode (NATIVE|BYO_LTI|BYO_SCORM|BYO_WEBHOOK), requires_practical_component, linked_policy_id (Program Catalog) |
CourseVersion | Native only | Versioned content; holders always take the currently published version |
Module / Lesson | Native only | Ordered content blocks: markdown, video URL/embed, quiz block |
Assessment | Native only | Pass threshold, attempt limit, question bank, shuffle |
Enrollment | Both paths | holder_key, course_id, source (self_serve | custodian_schedule), optional schedule_id link |
Attempt | Both paths | Native: per-attempt score/answers. Bridged: opaque — just records launch + result. |
CompletionEvent | Both paths | course_id, holder_key, result (PASS|FAIL), evidence_ref, occurred_at, source (NATIVE|LTI|SCORM|WEBHOOK) |
Path A — BYO-LMS bridge
Ordered by integration effort, all three optional per CourseDefinition — a custodian/issuer picks
whichever matches what they already run:
- LTI 1.3 launch (deep linking + resource link, with the standard signed return payload) — the industry-standard way institutional LMSes (Moodle, Canvas, Cornerstone, etc.) already expose external tool launch and grade return. Lowest lift for institutions that already support it.
- SCORM/xAPI (Tin Can) ingestion — for institutions that can only export a package or emit xAPI statements rather than support a live launch.
- Signed webhook fallback — for bespoke/in-house systems supporting neither: the custodian
registers an endpoint + shared secret and POSTs a
CompletionEvent-shaped payload. Reuses the same HMAC-signing pattern already established for issuer API keys, not a new auth primitive.
Path B — Attest-native authoring & delivery
Minimal viable slice, deliberately small:
- Authoring: lesson blocks (markdown / video URL / embed) + one assessment block per course (multiple-choice/true-false, pass threshold, shuffle, attempt limit). Draft → published versioning; holders only ever see published content.
- Delivery: the credential-wallet holder sees an “available to retake” / “assigned by
custodian” list, launches the native player, submits an attempt, gets an immediate result. A
CompletionEventis emitted internally — no network hop, same service.
Explicitly not in this first cut: cohort/instructor-led or synchronous delivery, discussion forums, gradebook/analytics dashboards, exporting native content as a portable SCORM package.
The practical-component guardrail
CourseDefinition.requires_practical_component is the one flag that keeps this design honest. If
set, a CompletionEvent from the online portion alone can never auto-close a schedule or
auto-issue a credential — it can only satisfy a sub-requirement, and the schedule stays
DUE/SCHEDULED pending the existing in-person attestation. This preserves
Custodian Compliance’s existing invariant — only a fresh
attestation closes a schedule — unchanged; it adds a new way to produce that attestation, it does
not weaken what counts as one.
Wiring into the existing custodian Schedule tab
- When a custodian’s
DUEobligation (CustodianReperformanceScheduleEntity) maps, via its policy, to aCourseDefinition, the Schedule tab gains an “Assign online course” action next to today’s Schedule / Reschedule / Cancel. - Assigning creates an
Enrollmentwithschedule_idset. ACompletionEventwithresult: PASS(and no unmet practical component) drives the existingCustodianReperformanceServiceclosing path automatically — a new caller into that service, not a change to its contract. - First-time (never-held) obligations from
FirstTimeScheduleDialogwork identically: anEnrollmentwith no prior credential, same as today’s schedule entity already models.
Auto-credentialing on pass
A CompletionEvent with result: PASS is the trigger for Core Engine to call Credential Issuance
and issue/renew the W3C VC — the same issuance path used everywhere else. Course delivery never
signs anything itself; private-key handling stays exactly as documented today (KMS/HSM-bound,
Data Models & Crypto).
Reconciling the existing “never an LMS” line
Custodian Workforce Compliance currently states gap tracking is “never a payroll/rostering/LMS function… HRIS/LMS integration is inbound and advisory only.” That remains true and is not contradicted by this doc: workforce-compliance still doesn’t roster people or run payroll, and an external HRIS/LMS is still inbound-only to it. What changes is that course delivery and retake is now a first-class Attest capability in its own right, documented here — workforce-compliance becomes one more consumer of it (a gap can now resolve via an assigned course, not only a proof link or a waiver) without owning it.
Open questions — require an explicit decision, not implementation detail
- Proctoring/anti-cheat. None is proposed here. Is an unproctored native online pass acceptable for regulated credentials, or must every native assessment be flagged advisory/low-stakes only until a proctoring answer exists?
- Content authoring ownership. Do issuers/custodians author native courses themselves via a builder UI, or does content get onboarded on their behalf initially?
- Native content portability. Should Attest-native content be exportable as SCORM so it isn’t a walled garden? Not designed here.
- Attempt fraud/collusion detection. The platform already has statistical anomaly detection for credentials (CLAUDE.md Roadmap Context) — whether that extends to assessment attempts is unresolved.
- Hosting/entitlement model for native video/content storage is a commercial question, out of this doc’s scope.
Explicitly out of scope (first cut)
- Any practical/in-person assessment delivery — stays 100% the existing custodian attestation flow.
- Payroll and rostering — named product direction, not scoped here; see Workforce Platform Expansion.
- Cohort/instructor-led/synchronous delivery.
- Cross-institution course marketplace or discovery.
Next step, if this gets prioritized
Add as a new backlog item in CLAUDE.md’s Roadmap Context (not launch-blocking — the current launch blockers are staging/alerting/tracing/security-audit/legal/backup-drill/load-testing/incident runbook and holder claim integrity), draft the 9th gRPC proto contract, and decide which BYO protocol ships first (LTI is the standard-effort default; the signed webhook is the fastest-to-build fallback). Do not implement from this doc alone without that backlog decision.