Files
Rotimi Best 2a82abfa21 docs(prd): add learning paths PRD with end-to-end HTML prototypes
Full spec for ordered, sequentially unlocked, bundle-priced learning
paths, plus 16 clickable prototypes (public org site, learner LMS, and
teacher workspace) built on the packages/ui tokens and the minimal
org-landing theme. The prototypes are the UX source of truth; start at
prototypes/learning-paths/index.html. Also adds the write-prd skill
codifying the clarify -> write -> prototype workflow.
2026-08-06 07:35:10 +01:00
..

Learning Paths PRD

Status

  • Draft (prototyped, ready for engineering review)

Prototypes — the UX source of truth

The UX of this feature must be taken from the prototype folder. Interactive HTML prototypes for every surface live in prototypes/learning-paths/ and are the approved design reference for layout, states (done / in progress / locked), copy, and navigation flow. When this document and a prototype disagree on a UI detail, the prototype wins.

Start here:

prototypes/learning-paths/index.html

Open it in a browser (open prototypes/learning-paths/index.html) — it maps all three journeys (visitor → learner → teacher) and every page cross-links so you can click through each flow end to end. Each page has a light/dark toggle.

The prototypes are built on the real design tokens (packages/ui/src/index.css for app pages via app-theme.css, the minimal org-landing theme for public pages via landing-theme.css) and encode every decision below.

Surface Files
Public (org site) org-landing.html, public-paths.html, public-path.html
Learner (LMS) lms-home.html, paths.html, path-detail.html, course-in-path.html
Teacher (admin) teacher-paths.html, teacher-path-setup.html, teacher-path-builder.html, teacher-path-people.html, teacher-path-analytics.html, teacher-path-landing-editor.html, teacher-path-certificate.html, teacher-path-settings.html

Purpose

Let orgs package multiple courses into an ordered, sequentially unlocked, bundle-priced learning path that is sold on the org's public site, completed step-by-step in the LMS, and finished with an optional path certificate. Reference experience: Coursera Professional Certificates (e.g. IBM Data Science).

Problem Statement

  • Courses can only be sold and taken individually; there is no way to sell a curriculum as one product with one price.
  • Programs (shipped) group courses for cohort admin purposes but are explicitly unordered, have no gating, no public page, no pricing, and no path-level certificate.
  • Instructors who run structured curricula cannot enforce prerequisites ("finish JS before React").
  • The org landing page has no product above the individual course, so multi-course offerings are invisible to visitors.

Confirmed Decisions

  1. New entity, separate from Programs. learning_path is its own table set. Programs remain unchanged (cohort hub + newsfeed). No migration.
  2. Terminology: "Learning Path" everywhere (admin, LMS, public site).
  3. Self-enrollment with bundle pricing. A path is public on the org landing site with one price. Enrolling (buy or free) auto-enrolls the learner in every course in the path.
  4. Show savings. The public page shows the bundle price against the summed individual course prices ("$199 $280 · Save $81 (29%)"). Toggleable per path.
  5. Unlock rule = lessons + exercises. Course N+1 unlocks when course N has every lesson complete AND every exercise submitted/passed. Sequential unlocking is a per-path toggle (off = take in any order).
  6. Path certificate is a teacher toggle with its own admin tab; issued automatically on full completion; appears in the learner's LMS Certificates page.
  7. Teacher workspace tabs: Courses (ordering), People, Analytics, Landing page, Certificate, Settings. No path newsfeed (Programs covers cohort communication).
  8. Visitor access is teacher-configurable: teaser only / full syllabus / syllabus + preview lessons.
  9. Public path page follows the Coursera professional-certificate shape: hero with stats + enroll CTA, what-you'll-learn, skills tags, sequential course cards with per-course outcomes and lesson outlines, certificate block, instructors, testimonials, FAQ accordion, pricing card.
  10. Org landing page: Learning Paths section sits ABOVE the Courses section. Plus a /paths catalog page and /path/[slug] detail page mirroring how /courses and /course/[slug] work today.
  11. Course independence: a course can be in multiple paths and remain individually sellable. Removing a course from a path or a learner from a path never deletes groupmember rows or progress.
  12. Statuses: ACTIVE (public + LMS), DRAFT (hidden while building), ARCHIVED (hidden everywhere, restorable).

Current-State Audit

Capability Current state Notes
Grouping above course program tables (unordered, admin-enrolled, newsfeed) Keep as-is; do not extend
Enrollment groupmember per course group; enrollInCourse() in apps/api/src/services/course/invite.ts Reuse for auto-enrollment
Course completion lesson completion + exercise submissions tracked per course Source data for the unlock rule
Public org site (org-site) routes + packages/ui/src/custom/org-landing-page/ (10 themes, default minimal) Mirror for paths section/list/detail
Course landing page theme course.svelte composer: hero → anchor nav → social proof → info blocks → curriculum → chips → instructor → reviews → pricing Path page reuses these shared components where possible
Payments course payment flow on paid enroll CTA (course-landing-page.svelte) Path enroll reuses the same flow with the path as the product
Certificates course certificate system + LMS Certificates page Add path-level issuance
Setup checklist pattern routes/(app)/org/[slug]/setup/+page.svelte + PercentRingProgress + Item.* Reuse for "Set up your path" and learner checklist view

Product Goals

  1. A visitor can discover a path on the org landing page, read a full Coursera-style path page, and self-enroll (paid or free).
  2. Enrolling in a path creates path membership and groupmember rows for every course in it.
  3. A learner sees paths in the LMS (home section + Learning Paths nav), opens a path hub with per-course progress and locked/unlocked states, and moves through courses in order.
  4. Inside a course that belongs to a path, the learner sees path context (position stepper, "next course unlocks when…").
  5. Teachers create a path via a setup checklist, order courses by drag, price the bundle, customize the public page, configure the certificate, and track a per-course funnel.
  6. Completing all courses issues the path certificate (when enabled).
  7. Programs, standalone courses, and existing enrollments are untouched (zero regression).

Non-Goals (v1)

  • Branching / non-linear paths (graphs, optional courses, parallel tracks).
  • Path-level newsfeed or community.
  • Subscriptions or installment pricing; coupon codes beyond what courses support.
  • Path templates, cloning, or cross-org sharing.
  • Per-learner overrides of the unlock rule (admin "skip ahead" can be a fast-follow).
  • Team/B2B seat purchasing.

Functional Requirements

1. Public — org landing site

Landing page section (above Courses; see org-landing.html):

  • Section heading "Learning Paths" + lead line; grid of path cards.
  • Path card: "Learning Path" chip, name, description, ordered course preview list (first 3 + "+N more"), meta row (course count, total hours, certificate), bundle price with struck-through sum.
  • "View all learning paths" → /paths.
  • Rendered per landing-page theme like the courses section (start with minimal; other themes can fall back to base styling).

Catalog page /paths (see public-paths.html): compact hero with search, filter sidebar (topic/tags, pricing, length), path card list, pagination. Mirrors (org-site)/courses.

Detail page /path/[slug] (see public-path.html), section order:

  1. Hero: chip, headline, subheadline (teacher-editable), instructor avatar stack, Enroll CTA with price + savings chip, sticky enroll box (price, savings, feature list, CTA, enrolled/rating counts).
  2. Stats row: courses, total hours, level, rating.
  3. Sticky anchor nav: About / Courses / Certificate / Instructors / Testimonials / FAQ.
  4. What you'll learn (outcome bullets) + Skills tags.
  5. Course series: one card per course in order — number, title, lesson count + duration, per-course outcome bullets, expandable lesson outline (lessons marked preview are openable when the teacher allows).
  6. Certificate block (when enabled).
  7. Instructors (auto-derived from course tutors, hideable).
  8. Testimonials (teacher-managed, hideable).
  9. FAQ accordion (teacher-managed, hideable).
  10. Pricing card + footer.
  • Enroll: free path → enroll flow like /course/{slug}/enroll; paid path → payment flow with the path as product. Requires auth like course enrollment.
  • Visitor access level (teaser / syllabus / syllabus+preview) gates sections 5's lesson detail.
  • JSON-LD Course/LearningResource schema like the course page.

2. Learner — LMS

Home (lms-home.html): "Learning Paths" section above Courses when enrolledPaths.length > 0; path cards show segmented per-course progress (one segment per course: done/current/locked) + overall %, and "Next: {course}". Course cards belonging to a path carry a "{path} · i/N" chip.

Nav: "Learning Paths" item in the LMS sidebar with enrolled count badge, shown only when enrolled in ≥1 path.

List /lms/paths (paths.html): tabs In progress / Completed / All; rows with segment bar, next course, Continue / View certificate.

Path hub /lms/paths/[id] (path-detail.html): the journey spine — a vertical rail of course nodes that fills as courses complete. Per course: state (Completed / In progress / Locked), progress bar, lesson+exercise counts, unlock note ("Unlocks after Course N"), certificate end-row.

Course in path (course-in-path.html): when the visited course belongs to a path the learner is in, wrap the course view with a path ribbon — back pill to the path, "Course 3 of 5", overall path %, horizontal stepper (done/current/locked) — and a "Next in your path" callout under the lesson showing what completing this course unlocks.

Unlock enforcement: locked courses are not openable from path surfaces; direct navigation to a locked course (URL) shows a "locked — finish {course} first" state. Learners enrolled in a course independently (outside the path) keep their normal access.

3. Teacher — admin dashboard

Org listing /org/[slug]/paths (teacher-paths.html): nav item "Learning Paths" after Courses; stat cards (active paths, enrolled learners, completions); table (name, courses, learners, status); Create path → creation modal → setup checklist.

Setup checklist /paths/[id]/setup (teacher-path-setup.html): Get Started pattern (ring + dots + item rows): name/describe ✓ auto, add courses, set order, set price, customize landing page, activate. Each step deep-links to its tab. Shown until complete; "Finish setup · N%" chip in the workspace top bar.

Path workspace /paths/[id]/* — course-style sidebar (back link, path identity + status, tabs):

Tab File Contents
Courses teacher-path-builder.html "Unlock courses in order" toggle; drag-to-reorder course rows (order, thumb, lessons/exercises/price, open/remove); add-course picker (org courses not in path); certificate end-row. Remove-course confirmation notes nobody is unenrolled.
People teacher-path-people.html Auto-enroll info banner; search + progress filter; table: learner, path progress bar, current course, enrolled date, actions (view, remove — preserves course access). Add learners (batch email, like course invites).
Analytics teacher-path-analytics.html Stat cards (enrolled, active, completion rate, avg time); per-course funnel with drop-off callouts; "stuck" list (lessons/exercises blocking most learners).
Landing page teacher-path-landing-editor.html Hero headline/subheadline; visitor access radio (teaser / syllabus / syllabus+previews); what-you'll-learn bullets; skills tags; instructors toggle (auto from course tutors); testimonials CRUD + toggle; FAQ CRUD + toggle; "View live page".
Certificate teacher-path-certificate.html Award toggle; requirements summary (all lessons + all exercises); title + issuer fields; live preview; awarded count.
Settings teacher-path-settings.html General (name, description, cover); Pricing (bundle price, currency, show-savings toggle with computed sum); Enrollment & flow (self-enrollment toggle, unlock-in-order toggle, auto-enroll toggle); Status radio (Active / Draft / Archived); Danger zone (delete path — courses & progress preserved).

4. Access control

  • Org admins manage all paths; path-level TUTOR role can manage content but not delete (mirror Programs roles: ADMIN/TUTOR/STUDENT via roleId).
  • Students: read path data they're members of; public endpoints serve ACTIVE paths only.
  • DRAFT paths: admin/tutor only. ARCHIVED: hidden everywhere, data preserved.

Technical Design

Data model (packages/db/src/schema.ts)

learning_path
  id uuid PK · organizationId FK(organization, cascade) · name varchar · slug varchar (unique per org)
  description text · coverImage text · status LEARNING_PATH_STATUS default 'DRAFT'
  cost numeric · currency varchar default 'USD' · showSavings boolean default true
  sequentialUnlock boolean default true · selfEnrollment boolean default true · autoEnroll boolean default true
  certificateEnabled boolean default true · certificateTitle text · certificateIssuer text
  landingPage jsonb  -- { headline, subheadline, visitorAccess: 'teaser'|'syllabus'|'preview',
                     --   outcomes: string[], skills: string[], showInstructors, testimonials: [...],
                     --   showTestimonials, faqs: [...], showFaqs }
  visitorAccess is inside landingPage; no relational IDs inside the jsonb (per repo rule)
  createdByProfileId FK(profile) · createdAt / updatedAt

learning_path_course
  id uuid PK · pathId FK(learning_path, cascade) · courseId FK(course, cascade)
  order integer NOT NULL          -- 1-based position; the gating sequence
  unique(pathId, courseId) · unique(pathId, order) · index(pathId)

learning_path_member
  id uuid PK · pathId FK(learning_path, cascade) · profileId FK(profile) · roleId FK(role)
  enrolledAt timestamptz · completedAt timestamptz nullable
  certificateIssuedAt timestamptz nullable · certificateId varchar nullable
  unique(pathId, profileId) · index(pathId) · index(profileId)

enum LEARNING_PATH_STATUS: ACTIVE | DRAFT | ARCHIVED

Notes:

  • No denormalized progress on learning_path_member beyond completedAt; per-course progress is computed from existing lesson-completion + exercise-submission data (already queried for course cards).
  • Savings figure is computed at read time from the sum of course.cost over path courses — never stored.
  • Schema work stops at a passing @cio/db build; migrations are handled outside this workflow.

Completion / unlock logic (service layer)

courseCompleteForPath(profileId, courseId):
  lessonsComplete  = completedLessons  == totalLessons
  exercisesComplete = submittedOrPassedExercises == totalExercises
  return lessonsComplete && exercisesComplete

unlockedCourses(path, profileId):
  if !path.sequentialUnlock → all
  else → courses[0..k] where k = first index whose course is not complete

Path completion: all courses complete → set completedAt; if certificateEnabled, issue certificate (id LP-XXXX-XXXX), set certificateIssuedAt. Evaluate on lesson/exercise completion events for members of paths containing that course (reuse the pattern used by course-completion side effects).

API routes (apps/api/src/routes/learning-path/)

Follow the standard layering (validation in packages/utils/src/validation/learning-path/, queries in packages/db/src/queries/learning-path/, services in apps/api/src/services/learning-path/). Mount as a single root segment: .route('/learning-path', learningPathRouter).

Method Path Auth Description
GET /learning-path org admin List org paths (admin)
POST /learning-path org admin Create (name, description) → DRAFT
GET /learning-path/:pathId member/admin Detail incl. ordered courses + caller progress
PUT /learning-path/:pathId admin/tutor Update settings/pricing/landingPage/status
DELETE /learning-path/:pathId org admin Delete (courses/progress preserved)
POST /learning-path/:pathId/courses admin/tutor Add course (+ auto-enroll existing students)
PUT /learning-path/:pathId/courses/order admin/tutor Reorder (array of courseIds)
DELETE /learning-path/:pathId/courses/:courseId admin/tutor Remove (no unenrollment)
GET /learning-path/:pathId/members admin/tutor Members + per-course progress
POST /learning-path/:pathId/members admin/tutor Batch add (email+role) + auto-enroll
DELETE /learning-path/:pathId/members/:memberId admin/tutor Remove (course access preserved)
GET /learning-path/:pathId/analytics admin/tutor Funnel + stuck items
POST /learning-path/:pathId/enroll auth user Self-enroll (free) / post-payment callback (paid)
GET /organization/learning-paths/enrolled auth user LMS: caller's paths with progress + unlock states

Public (org-site loaders, no auth): list ACTIVE paths for landing/catalog; get path by slug with landingPage content filtered by visitorAccess.

Frontend plan (dashboard)

Follow CLAUDE.md conventions: types in features/learning-path/utils/types.ts inferred from the API, API classes in features/learning-path/api/*.svelte.ts, thin components, all copy in en.json under "learningPath", ui: prefix for theme colors.

  • Admin: nav item in org-navigation.ts after Courses; routes org/[slug]/paths/+page.svelte and paths/[id]/{setup,courses,people,analytics,landing,certificate,settings} with a PathSidebar mirroring the course sidebar pattern. Reuse Item.*, PercentRingProgress, Field.*, existing drag-reorder approach from lesson ordering if present.
  • LMS: lms/paths + lms/paths/[id]; path ribbon injected in the course layout when course ∈ caller's path; reuse course-progress-card math for per-course %.
  • Public: paths section added to org landing themes (start minimal), (org-site)/paths and (org-site)/path/[slug] mirroring the courses equivalents; reuse CourseSectionNav, CourseSocialProof, CourseCurriculum-style rows, CoursePricing card, LandingButton, footer.

Build verification

pnpm --filter @cio/utils build && pnpm --filter @cio/db build && pnpm --filter @cio/api build
pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build
pnpm format:check

Implementation Order

  1. Schema + validation + queries (learning_path* tables, Zod schemas, query layer).
  2. API: CRUD + course ordering + members + enroll service (auto-enroll transaction, ON CONFLICT DO NOTHING idempotency) + unlock/completion service + enrolled-paths endpoint.
  3. Admin UI: listing → workspace shell + Courses tab → Settings → People → setup checklist → Landing editor → Certificate → Analytics.
  4. LMS: enrolled section on home + nav, list, path hub (pick journey or checklist), course-in-path ribbon + lock enforcement.
  5. Public: landing section (minimal theme) → catalog → detail page → enroll flow (free first, then paid via existing payment flow).
  6. Certificate issuance + LMS Certificates page integration.

Acceptance Criteria

  1. Teacher can create a path, add + reorder courses, set a bundle price, and activate it; the setup checklist reflects real completion.
  2. ACTIVE paths appear on the org landing page above courses, in /paths, and at /path/[slug] with all enabled sections; DRAFT/ARCHIVED paths do not.
  3. Savings displays as bundle price vs summed course prices and updates when course prices change.
  4. Visitor access level correctly gates lesson outlines/previews for non-enrolled visitors.
  5. Enrolling (free or paid) creates the member row and groupmember rows for all path courses, idempotently.
  6. With sequential unlock on, course N+1 is locked until course N's lessons AND exercises are complete — enforced in UI and API; toggle off restores free order.
  7. Learner LMS shows path progress (segments + %), locked states, and the in-course path ribbon; direct URL access to a locked course is blocked with an explanatory state.
  8. Completing all courses sets completedAt and (when enabled) issues the path certificate, visible in LMS Certificates.
  9. Removing a course from a path or a member from a path never deletes course enrollments or progress.
  10. Adding a course to a path with existing students auto-enrolls them in that course.
  11. Analytics funnel counts match member course-completion data.
  12. Programs and standalone courses behave exactly as before.
  13. All user-facing strings use translation keys; all builds and pnpm format:check pass.

Risks and Mitigations

  • Unlock-rule evaluation cost (checked on every lesson/exercise completion): scope the check to paths containing that course with the learner as member; index learning_path_course.courseId.
  • Auto-enroll transaction size for large paths/cohorts: batch inserts with ON CONFLICT DO NOTHING; >500 members → background job (same mitigation as Programs).
  • Price drift: savings computed at read time, so course price changes are always reflected; if a course in an active path becomes free/pricier, surface a notice in the Settings pricing section.
  • Learner already enrolled in a member course independently: idempotent enrollment; their existing progress counts toward the unlock rule immediately (decide: acceptable and desirable).
  • Locked-course deep links shared between learners: API must enforce the lock (not just UI), returning a typed error the frontend renders as the locked state.
  • Landing theme sprawl (10 themes): v1 ships the paths section/pages for the base + minimal theme with a neutral fallback for others, mirroring how course landing tokens are layered.