LMS Hierarchy Structure¶
Status: Design — authoritative for Phase 2 (data model) and Phase 7 (entry pagination) implementation.
This document describes the hierarchical content model of the LMS. All course structure is CMS-authored; the database only tracks learner progress against entries, never the structure itself.
Overview¶
The LMS organises learning content into a fixed three-level hierarchy:
There is no nesting beyond Entry. The hierarchy is entirely sourced from edX CMS and fetched at runtime via src/lib/cms/client.ts under ISR (revalidate = 60). The DB has no courses, modules, or entries tables — only entry_progress to track what a learner has done.
Hierarchy Levels¶
Course¶
- Top-level container for all learning content.
- Identified by
course_slug, which is unique within a tenant (tenant_id+course_slugis the composite key for all DB lookups). - Sourced entirely from edX CMS; never stored in the application database.
- A course belongs to exactly one tenant; multi-tenant scoping is enforced on every query.
Module¶
- Mid-level grouping within a course.
- Identified by
module_slug, unique within its course. - Carries a CMS-authored
_orderfield used to sort modules ascending within their course. - Modules are the visual grouping unit in
CourseSidebar(see toggleable-navigation.md). When a module has child pages,mapHierarchyToCoursealso emits a module overview entry (the module's own CMS page) as the first entry in that module. Non-leaf folders below module level emit folder overview entries in depth-first order; the sidebar renders nested collapsible folder groups. Prev/next traversal still crosses module boundaries transparently.
Entry¶
- The atomic learning unit; the smallest navigable item.
- Identified by
entry_slug, unique within its course (not globally — two different courses may share the same entry slug). - Carries a CMS-authored
_orderfield used to sort entries ascending within their module. - Quiz entries sourced from the
structured-quizwidget preserve this CMS_orderthrough the LMS adapter (src/lib/cms/quiz-adapter.ts), so lesson and quiz sequencing follows one canonical ordering contract. - Carries a
_typediscriminator that determines how it is rendered and how it is completed.
Entry Types¶
| Type | Renderer | Completion mechanism |
|---|---|---|
lesson |
Portable Text body (RSC) | Explicit "Mark complete" click by the learner |
quiz |
QuizPlayer (client component) |
Pass (score ≥ threshold) or maxAttempts exhausted |
assignment |
Stub — Phase 7+1 deferred | Manual "Mark complete" click (placeholder behaviour) |
Quiz internals (question types, scoring, retry policy) are out of scope here; see quiz-system.md. The pagination system only consumes the binary completion signal the quiz subsystem emits. Full entry lifecycle is specified in entry-pagination.md.
Slug Conventions¶
- Course slug — unique within a tenant; used as the primary route segment (
/learn/course/[courseSlug]). It is the lowercase CMS project acronym, with no whitespace. - Entry slug — unique within a course; used as the secondary route segment (
/learn/course/[courseSlug]/entry/\[entrySlug\]). Same format rules apply. - Module slug — unique within a course; used as a
data-testidprefix (sidebar-module-<moduleSlug>) and, when the module has child pages, as the overview entry slug at/learn/course/[courseSlug]/entry/[moduleSlug]. - Folder slug — non-leaf nodes below module level also emit folder overview entries at
/learn/course/[courseSlug]/entry/[folderSlug]. Deeper leaves record ancestor folder slugs onbreadcrumbarrays for sidebar grouping and chrome. - Numeric IDs are never used in public-facing URLs. All routing relies on slugs.
- DB lookups that touch course or entry data always use the composite key
(tenant_id, course_slug)or(tenant_id, course_slug, entry_slug).
Ordering & Sequencing¶
CMS _order field¶
Both modules and entries carry an integer _order field authored in the CMS. Ordering is:
- Entries: sorted ascending by
_orderwithin their module. - Modules: sorted ascending by
_orderwithin their course.
_order is 1-indexed per scope (first module in a course has _order: 1, first entry in a module has _order: 1). Gaps are allowed — do not assume consecutive integers.
Flattened sequence¶
For navigation purposes the hierarchy is collapsed into a single ordered list: all entries of module 1 in ascending _order, then all entries of module 2, and so on. This flattened sequence is the canonical path through a course and is the basis for prev/next resolution, resume logic, and gating checks.
The helper function getEntryPagination(entries, currentEntrySlug) in src/lib/course/pagination.ts operates on this flattened list and returns { previous: Entry | null, next: Entry | null }. Module boundaries are transparent to the caller.
CMS vs DB Boundary¶
| What | Where | Cache policy |
|---|---|---|
| Course structure (Course, Module, Entry metadata, Portable Text body) | edX CMS | ISR revalidate = 60; tagged course:<slug> |
Learner progress (completed_at, last_viewed_at) |
Kysely DB (entry_progress table) |
Per-request; never ISR |
| Enrollment state | Kysely DB (enrollments table) |
Per-request; never ISR |
CMS content is fetched via src/lib/cms/client.ts. All methods validate responses against Zod schemas in src/schemas/. Admin-triggered content invalidations call revalidateTag('course:' + courseSlug).
The entry_progress table schema is defined in implementation.md Phase 2 and reproduced in entry-pagination.md §Data Model.
TypeScript Shapes¶
Interfaces are defined in src/types/ (no inline structural types per root AGENTS.md / .cursor/rules). The minimal shapes that flow through the hierarchy:
interface Course {
slug: string;
title: string;
modules: Module[];
}
interface Module {
slug: string;
title: string;
_order: number;
entries: Entry[];
}
interface Entry {
slug: string;
title: string;
_order: number;
_type: "lesson" | "quiz" | "assignment";
}
These are extended in the entry-pagination layer with per-learner progress fields (e.g., completed_at) and in the quiz layer with attempt state. The CMS client returns Course objects fully populated with nested Module[] and Entry[] arrays.
Related Documents¶
- entry-pagination.md — ordering rules, sequential gating, prev/next resolution, completion semantics.
- toggleable-navigation.md — how modules and entries are rendered in the collapsible sidebar.
- course-enrollment-system.md — enrollment guard that controls access to the entry layer.
- quiz-system.md — quiz entry completion handoff.
- implementation.md — Phase 2 (DB migrations,
entry_progressschema), Phase 7 (entry playback binding spec).