Appearance
Course Service API
SVC-007 Student Route Proof
See Student Course Route Proof for the accepted bounded proof covering enrollment-gated student list/detail/progress, published lesson/material projection, static native route examples, and the remaining live/browser/default-route gate.
Current Endpoints
GET /healthzGET /readyzGET /v1PUT /v1/courses/{courseId}/snapshotGET /v1/coursesPOST /v1/coursesGET /v1/courses/search-projectionsGET /v1/courses/learning-catalog-eventsGET /v1/courses/integration-contracts/question-examPOST /v1/internal/courses/{courseId}/quiz-links/{referenceId}/attempt-contextGET /v1/courses/{courseId}PATCH /v1/courses/{courseId}DELETE /v1/courses/{courseId}GET /v1/courses/{courseId}/sectionsPOST /v1/courses/{courseId}/sectionsPATCH /v1/courses/{courseId}/sections/{sectionId}DELETE /v1/courses/{courseId}/sections/{sectionId}GET /v1/courses/{courseId}/lessonsPOST /v1/courses/{courseId}/lessonsGET /v1/courses/{courseId}/lessons/{lessonId}PATCH /v1/courses/{courseId}/lessons/{lessonId}DELETE /v1/courses/{courseId}/lessons/{lessonId}POST /v1/courses/{courseId}/lessons/{lessonId}/videoDELETE /v1/courses/{courseId}/lessons/{lessonId}/videoGET /v1/courses/{courseId}/lessons/{lessonId}/question-linksPOST /v1/courses/{courseId}/lessons/{lessonId}/question-linksPATCH /v1/courses/{courseId}/lessons/{lessonId}/question-links/{referenceId}DELETE /v1/courses/{courseId}/lessons/{lessonId}/question-links/{referenceId}GET /v1/courses/{courseId}/materialsPOST /v1/courses/{courseId}/materialsGET /v1/courses/{courseId}/materials/{materialId}PATCH /v1/courses/{courseId}/materials/{materialId}DELETE /v1/courses/{courseId}/materials/{materialId}GET /v1/courses/{courseId}/quiz-linksPOST /v1/courses/{courseId}/quiz-linksPATCH /v1/courses/{courseId}/quiz-links/{referenceId}DELETE /v1/courses/{courseId}/quiz-links/{referenceId}GET /v1/courses/{courseId}/enrollmentsPOST /v1/courses/{courseId}/enrollmentsPATCH /v1/courses/{courseId}/enrollments/{studentId}DELETE /v1/courses/{courseId}/enrollments/{studentId}GET /v1/student/coursesGET /v1/student/courses/{courseId}POST /v1/student/courses/{courseId}/lessons/{lessonId}/progressPOST /v1/student/courses/{courseId}/lessons/{lessonId}/video-progressPOST /v1/student/courses/{courseId}/materials/{materialId}/viewGET /v1/public/coursesGET /v1/public/courses/{slug}
Native Course Foundation
Phase 8 course-service starts with course catalog records, sections, lessons, material metadata, enrollments, and basic student progress. This is an internal /v1 foundation, not a public /api/courses*, /api/student/courses*, or /api/public/catalog* cutover.
Legacy evidence:
node-platform/apps/api/src/modules/courses/courses.controller.ts:42-334maps teacher/admin course CRUD, section, lesson, video, material, enrollment, question, quiz, and report routes.node-platform/apps/api/src/modules/courses/courses.controller.ts:336-386maps student course list/detail/mastery/recommendation/purchase/progress routes.node-platform/apps/api/src/modules/courses/courses.service.ts:23-169delegates course routes toAppDataServiceand wallet purchase.node-platform/apps/api/src/modules/app-data/app-data.courses-support.ts:41-160defines manager checks, active enrollment checks, topic validation, and response expansion.node-platform/apps/api/src/modules/app-data/app-data.courses-catalog.ts:41-262implements legacy course list/detail/create/update/archive and section CRUD.node-platform/apps/api/src/modules/app-data/app-data.courses-lessons.ts:41-477implements lesson/material CRUD, video metadata attachment, and cleanup behavior.node-platform/apps/api/src/modules/app-data/app-data.courses-enrollment.ts:41-475implements enrollment CRUD, student course projection, lesson progress, video progress, and material views.node-platform/apps/api/src/modules/app-data/app-data.courses-assessment.ts:41-708implements question links, quizzes, report, mastery, and recommendation; these are deferred from P8-005.node-platform/apps/api/src/modules/app-data/app-data.courses-files.ts:41-119implements video/material content file access; streaming remains document-service/gateway work.node-platform/apps/api/src/modules/app-data/app-data.public.ts:19-392exposes public course home/catalog/detail projections.node-platform/apps/api/prisma/schema.prisma:920-1188definesCourse,CourseSection,CourseLesson,CourseMaterial,CourseEnrollment, progress/view tables, question links, and quizzes.node-platform/packages/shared/src/index.ts:1278-1379defines course, section, lesson, material, enrollment, and progress validation schemas.node-platform/apps/web/components/course/course-crud-client.tsx:52-307consumes teacher/admin course CRUD, status, archive, and report routes.node-platform/apps/web/components/course/course-management-client.tsx:108-316consumes course detail, sections, lessons, materials, video, question links, quizzes, and enrollments.node-platform/apps/web/components/course/student-course-client.tsx:131-170consumes student course detail, recommendation, lesson/video progress, and material views.
Native contract:
PUT /v1/courses/{courseId}/snapshotupserts service-owned course data from legacy backfill, including nested sections, lessons, materials, and enrollments when supplied.GET /v1/coursessupportsq,status,organizationId,instructorId,subjectId,visibility,page, andlimit;X-Actor-Id/X-Actor-Rolescopes non-admin reads.POST /v1/coursescreates a course with statusDRAFTby default. Teachers default to themselves as instructor; admins must provide an instructor id until profile-service adapters can choose a fallback.GET/PATCH/DELETE /v1/courses/{courseId}preserve detail, partial update, and archive semantics. Delete setsstatus=ARCHIVEDand clearsfeaturedAt.- Section routes preserve course-scoped create/update/delete and
orderIndexordering. - Lesson routes preserve course-scoped create/update/delete, optional section/topic ids,
preview,publishedtopublishedAt, content fields, video metadata, and deletion cleanup of lesson-owned materials/progress. - Material routes preserve metadata CRUD, optional lesson binding,
preview,publishedtopublishedAt, and deletion cleanup of material views. File bytes remain document-service/object-storage work. - Lesson-question routes are
ADMIN/owning-TEACHERmanagement APIs only. They store a question-bank public id, placement role/order, and an optional copied title. A repeated(lessonId,questionId)save updates that placement without duplicating the reference. They never fetch question text, answers, solutions, taxonomy, or usage state from another database. - Quiz routes are
ADMIN/owning-TEACHERmanagement APIs only. They store an exam-service public id, optional same-course section/lesson placement, type/required score/order, and an optional copied title. A repeated(courseId,examId)save updates that placement without duplicating the reference. Lesson deletion cascades question references and unbinds quiz references; section deletion unbinds quiz section placement. These references are intentionally absent from student/public detail projections. POST /v1/internal/courses/{courseId}/quiz-links/{referenceId}/attempt-contextis an internal Gateway-only decision, not a learner route. It validates the authenticatedSTUDENThas active enrollment, the Course is published, the linked lesson is published and topic-bound, and the placement's localexamIdmatches the already resolved Exam runtime. Its bounded result carries only course/reference/lesson/student/organization ids,examId, and Course's validation timestamp. The adapter usesexamIdonly to bind the internal handoff; Attempt discards it before persisting the analyticscourseContextsnapshot. Browser request data can identify a selected Course quiz placement but cannot assert this owner decision.- Enrollment routes preserve upsert by
(courseId,studentId), status, paid amount, expiration, grant actor, and note. - A successful enrollment grant optionally emits best-effort analytics event
course.enrollment.granted.v1to analytics-servicePOST /v1/analytics/eventswhenANALYTICS_SERVICE_URLorANALYTICS_SERVICE_BASE_URLis configured. The event usessourceService=course-service,sourceEventId=course_enrollment:{enrollmentId}:granted,entity=CourseEnrollment, and a bounded payload with course/student/status, paid amount, expiration, and grant actor. Analytics delivery failure does not roll back the enrollment write. RecordLessonProgressnever calls Analytics on the learner request path. Migration000007_course_learning_progress_outbox.sqlwrites a durable, replayablecourse.lesson_progress.saved.v1record for organization-scoped progress in the same Course database operation. Each mutable saved state has its own opaque source event id and a bounded payload containing only course, lesson, student, status, percent, and timestamps; it excludes lesson content, media URLs, storage keys, and profile PII.RecordVideoProgressandRecordMaterialViewoptionally emitcourse.video_progress.saved.v1andcourse.material_view.saved.v1with timestamped replay ids for mutable rows. Payloads include progress counters and display metadata only; they exclude lesson content, media URLs, storage keys, file URLs, and profile PII.- Student course routes preserve active enrollment checks, published-course requirement, published lesson/material filtering, lesson progress status derivation, video progress auto-completing lessons at 95 percent, and material view increments. Lesson-attached materials are returned only when the owning lesson is visible to the student.
- Public course routes expose published/public/slugged course projections for gateway adapters. Preview materials attached to lessons are returned only when the owning lesson is also part of the public preview projection.
GET /v1/courses/search-projectionsexposes copiedCOURSEsearch documents for search-service rebuilds. It uses the same course list filters and actor scope asGET /v1/courses;allOrgs=1is honored only for admin actors. Whenstatusis omitted, archived courses are included so full rebuilds preserve archive state.GET /v1/courses/learning-catalog-eventsis the direct owner-service replay and backfill feed for Course learning-catalog snapshots, not a public or default Gateway route. It is restricted to anADMINservice identity, scoped to one organization unless an unscoped service identity explicitly usesallOrgs=1, and uses theafterOccurredAtplusafterSourceEventIdcursor pair. The optional000006Course outbox delivers the same immutable snapshots to Analytics without polling this route; delivery only records an input event and never materializes Analytics projections by itself.000005_course_learning_catalog_events.sqlrecords an immutablecourse.learning_catalog.changed.v1event in the same Course transaction as course publish/unpublish, lesson topic/order/publish changes, and quiz placement changes. The event has a per-course/per-organization catalog revision and emits anUPSERTonly for a published course; all other states produce a tombstone. It contains only published lessons with a topic id and quiz references placed on those lessons. It excludes lesson content, question data, answer keys, file/media URLs, profile data, and course-level or topicless quiz placements that cannot safely contribute to mastery.000003_course_search_projection_outbox.sqlalso writes a durable, service-ownedsearch.projection.changed.v1envelope for copiedCOURSEstate. Parent course changes and count-affecting lesson, material, enrollment, and lesson-progress mutations advance a per-course/per-organization revision in the same transaction. An organization move emits a DELETE for the old search scope and an UPSERT for the new one. Archive remains an UPSERT withARCHIVED; only physical deletion produces a DELETE tombstone.- The copied event contains searchable course fields and aggregate counts only. It excludes lesson/material raw content, video/view state, student ids, file URLs, and storage keys. No public API endpoint or Gateway route is added.
000004_course_question_exam_references.sqlowns the two local reference tables and advances the course search projection when their counts change. The event exposes aggregatequestionLinkCountandquizCountonly; it does not include external ids or copied titles.- The dispatcher is disabled by default. Set
COURSE_SEARCH_PROJECTION_EVENT_TRANSPORT=natsplusCOURSE_SEARCH_PROJECTION_EVENTS_NATS_URL(orSEARCH_PROJECTION_EVENTS_NATS_URL/NATS_URL) to enable it. OptionalCOURSE_SEARCH_PROJECTION_EVENT_NATS_SUBJECT,SEARCH_PROJECTION_EVENTS_NATS_STREAM, andCOURSE_SEARCH_PROJECTION_OUTBOX_{POLL_INTERVAL_MS,BATCH_SIZE,MAX_ATTEMPTS,RETRY_DELAY_MS,LEASE_MS}tune the local publisher; a Postgres store is required.
Search projection response shape:
json
{
"success": true,
"data": {
"sourceService": "course-service",
"entityType": "COURSE",
"documents": [],
"nextCursor": null,
"hasMore": false,
"meta": {
"page": 1,
"limit": 20,
"total": 0,
"totalPages": 1
}
},
"message": "OK"
}Search-service stores these copied rows in its own index tables and must not read course-service tables directly.
Authorization model:
- Native service accepts
X-Actor-Id,X-Actor-Role, andX-Organization-Id; gateway remains responsible for JWT verification and public route RBAC. ADMINmay manage all courses unless an organization filter is explicitly applied.TEACHERmay manage courses whereinstructorIdorcreatedByIdequals the actor.STUDENTmay read course detail and record progress only with an active, unexpired enrollment andPUBLISHEDcourse status.PARENTis not part of the P8-005 course foundation.
Database:
services/course-service/migrations/000002_courses.sqlcreatescourses,course_sections,course_lessons,course_materials,course_enrollments,course_lesson_progress,course_video_progress, andcourse_material_views;000004_course_question_exam_references.sqladds only course-owned placement references.- Cross-service ids such as
organization_id,subject_id,topic_id,instructor_id,created_by_id,uploaded_by_id, andstudent_idare stored as public ids; course-service does not query other service databases. course_lessons.video_asset_idand material storage fields store document-service references only.GET /v1/courses/integration-contracts/question-examrecords the native management-only reference boundary. Lesson-question references storecourseId,lessonId,questionId, role/order, and optional copied display title, while question-bank-service remains canonical for question body, answers, solutions, taxonomy, and usage counters through owner APIs such asGET /v1/questions/{id}andPUT /v1/questions/usage-counts. Quiz references store onlycourseId, optional same-coursesectionId/lessonId,examId, type/required score/order, and optional copied title, while exam-service remains canonical for exam authoring, snapshots, grading rules, attempts, and runtime access through owner APIs such asGET /v1/exams/{id}andPUT|GET /v1/exams/{examId}/question-snapshots.- Owner API validation, Gateway/BFF adapters, learner quiz delivery, wallet purchase records, and canonical media metadata remain deferred. Course mastery/recommendation/report work is explicitly planned as the copied-owner contract in Course Learning Projection Contract: Analytics owns the read model, Course owns catalog/progress snapshots, and Attempt must carry a Course-validated context. An
examIdalone is not a valid course association.
Rollback for this native slice:
- Keep
/api/courses*,/api/student/courses*, and/api/public/catalog*routed to legacy. - Disable gateway callers for
/v1/courses*,/v1/student/courses*, and/v1/public/courses*. - Keep default question-link/quiz routes, report, mastery, recommendation, wallet purchase, and file streaming on legacy until owner validation and dedicated Gateway/BFF adapters land.
- Drop course-service local course tables with the migration down step if local test data must be reset.
Gateway rehearsal:
deploy/gateway/routes.student-courses-read-native-example.json and deploy/gateway/routes.student-courses-read-native-localhost-example.json provide non-default route-table rehearsals for student course reads. They route only:
GET /api/student/coursesGET /api/student/courses/{courseId}
to course-service with gateway auth, STUDENT role enforcement, and organization header injection. Recommendation, purchase, lesson progress, video progress, material view, teacher/admin course management, public catalog, broad student-course/course/public routes, and fallback stay legacy-proxied until the missing adapters are explicit.
Run make test-student-course-routes before any live or browser rehearsal.
Student-course progress gateway rehearsal:
deploy/gateway/routes.student-course-progress-native-example.json and deploy/gateway/routes.student-course-progress-native-localhost-example.json provide non-default route-table rehearsals for student course progress writes. They route only:
POST /api/student/courses/{courseId}/lessons/{lessonId}/progressPOST /api/student/courses/{courseId}/lessons/{lessonId}/video-progressPOST /api/student/courses/{courseId}/materials/{materialId}/view
to course-service with gateway auth, STUDENT role enforcement, and organization header injection. Student course list/detail are covered by the separate read rehearsal and remain legacy in this progress-only table. Mastery, recommendation, purchase, teacher/admin course management, public catalog, broad student-course/course/public routes, and fallback stay legacy-proxied until the missing adapters are explicit.
Run make test-student-course-progress-routes before any live or browser rehearsal.
Non-goals for P8-005:
- Public gateway adapter/cutover.
- Wallet purchase and AZ Credit consumption.
- Public/Gateway question-link or quiz/exam delivery beyond the native management-only owner-boundary references.
- Mastery recalculation, recommendations, and full course reports.
- File byte streaming and signed URL generation.
- Cross-service validation against school-service, profile-service, question-bank-service, exam-service, document-service, or wallet services.