Roadmap¶
This page centralises roadmap items for the LMS: deferred work that came out of the phased implementation plan, and creator/marketplace feature proposals that have not been committed to a milestone.
Most entries are documentation-derived rather than independently code-verified. Each item links back to the source where it is currently described.
Status labels:
- Planned: Accepted future work described in a milestone or reference document.
- Partial: Some infrastructure exists, but the feature is not complete.
- Proposal: High-value idea not yet committed to a milestone.
- Documentation: Docs work that should stay visible until written or reviewed.
Phase 3.5 — Email Provider & Account Flows¶
Shipped in Milestone 1. Operational summary: Milestone 1 — Phase 3.5. Runtime detail and sequence diagrams: Authentication.
Implementation Plan Follow-Ups¶
These items are referenced in [reference/implementation.md](reference/implementation.md) but are not yet complete. Treat the implementation plan as authoritative for sequencing; the entries here exist so the roadmap is the single discovery surface for outstanding work.
- Planned: Phase 6 — Enrollment Flow. Build the
app/\[locale\]/learn/course/\[slug\]/enroll/route, theenrollInCourseserver action (upsert + audit +revalidateTag('progress:<userId>')), the enrolled branch of the course home, and the provider-neutralsrc/server/payments/seam returning501 Not Implementedfor paid courses. Today the[EnrollCTA](https://github.com/open-learning-hub/learning-platform/blob/main/apps/lms/src/components/site/enroll-cta.tsx)links to a 404. Source:reference/implementation.md— Phase 6,[reference/course-enrollment-system.md](reference/course-enrollment-system.md). - Planned: Phase 7 — Entry Pagination & Progress. Add
src/lib/course/pagination.tsandsrc/lib/course/gating.ts(getEntryAccess), theapp/\[locale\]/learn/course/\[slug\]/entry/\[entrySlug\]/route,markEntryComplete/recordEntryViewserver actions,CourseSidebar, admin preview-as-student mode, and the resume target. Source:reference/implementation.md— Phase 7,[reference/entry-pagination.md](reference/entry-pagination.md),[reference/toggleable-navigation.md](reference/toggleable-navigation.md). - Planned: Phase 7.5 — Assignment Entry Placeholder. Route and render
_type: 'assignment'entries through the lesson Portable Text path with manualMarkCompleteButtonand explicit "submissions not yet implemented" copy. Depends on Phase 7. Source:reference/implementation.md— Phase 7.5. - Planned: Search inside a single course's entries. Add a learner-facing search (or filter) scoped to one enrolled course's module/entry tree — for example in
CourseSidebaror the enrolled course outline — so learners can jump to a lesson or quiz by title without scrolling the full hierarchy. Distinct from catalog-wide discovery in F3. Deferred from Milestone 2 Phase 7 scope. Source:[planning_milestone_2.md](planning_milestone_2.md),[reference/toggleable-navigation.md](reference/toggleable-navigation.md). - Planned: Per-tenant well-known files, OG/Twitter cards, and CMS revalidation webhook (Phase 10.5). Source:
reference/implementation.md— Phase 10.5. - Implemented: PostHog observability (error tracking, product analytics, OpenTelemetry log forwarding) via
@open-learning-hub/observability, with a consent gate and privacy-safe defaults. Source: T-015 PostHog observability,admin/observability.md. - Planned: Structured logging redaction breadth and rate-limit driver swap from in-memory to Upstash for production (Phase 10). Source:
reference/implementation.md— Phase 10. - Implemented: i18n CI check (
scripts/check-i18n.mjs, wired into rootnpm run check) and locale parity acrossmessages/{en,es,fr,de,pt,zh}.json(Phase 11). Script details: Root Scripts Reference. Source:reference/implementation.md— Phase 11. - Planned: Playwright e2e suites for auth, enrollment, entry pagination, quiz, admin, theme, and WCAG (Phase 11). Source:
reference/implementation.md— Phase 11. - Partial: Phase 3.5 account-flow e2e coverage. Unit coverage exists in
[tests/unit/auth/tokens.test.ts](https://github.com/open-learning-hub/learning-platform/blob/main/apps/lms/tests/unit/auth/tokens.test.ts); Playwright covers sign-up notice and forgot-password confirmation in[tests/e2e/auth-flows.spec.ts](https://github.com/open-learning-hub/learning-platform/blob/main/apps/lms/tests/e2e/auth-flows.spec.ts). Missing: sign-up → verify → sign-in, magic-link consume, invitation acceptance perreference/implementation.md— Phase 3.5 item 9. - Planned: OpenAPI / Swagger pointer in
[.cursor/rules/050-apis.mdc](rules/apis.md)and Phase 10.5 surface verification against the rule's caching policy table (Phase 12). Source:reference/implementation.md— Phase 12. - Planned: Centralize seed/demo/e2e test passwords via
SEED_DEMO_PASSWORDinstead of hardcoded literals in seeds, Playwright specs, and unit tests. Source: T-028 seed/demo password env (done).
Cross-App Theme Follow-Up¶
Dark Mode Compatibility For CMS Project Themes (Implemented)¶
Problem. The LMS theme selector correctly applies .dark to the document, but CMS project-theme colour variables are emitted after the shared design-token stylesheet. The site project uses a global :root block, while course projects set variables on [data-course-project-theme]; both override the inherited dark palette, leaving LMS surfaces visually light when dark mode is selected.
Shipped behavior.
- CMS-authored colour tokens apply only in light mode. In dark mode, site and course surfaces fall back to the shared neutral dark design-token palette.
- CMS-authored typography remains active in both modes: font family, heading font family, base font size, and line height.
- Author-written
customCssremains active in both modes as an explicit escape hatch. - The CMS workarea preview and static exports retain their current unscoped project-theme behavior because they do not use the LMS chrome theme selector.
Implementation pointers.
apps/cms/src/lib/theme/theme-css-generator.tsnow supports an opt-in LMS light-only color mode: colours emit under:root:not(.dark)(site) or:root:not(.dark) [data-course-project-theme](course).- CMS v1
expand=projectThemeresponses for page detail and page hierarchy opt into that mode; CMS workarea preview and static export keep their existing unscoped behavior. - Regression coverage now includes generator and CMS route tests for the new selectors; LMS dark-mode behavior remains unchanged.
- The font-size preference cascade remains intact:
html[data-font-size]still overrides the site theme's base font size multiplier.
Sources: reference/course-theme.md, CMS theme system (apps/cms/docs/reference/theme-system.md), and CMS roadmap (apps/cms/docs/roadmap.md#reference-doc-follow-ups).
Dark Branding Variants For CMS Themes (Planned — Phase 2)¶
Decision. Phase 1 ships light-only colour scoping for LMS safety. A second phase may add true dark branding variants for CMS presets and project overrides.
Accepted direction (hybrid).
- Keep explicit author control: support a dark palette alongside the current palette.
- Provide derived dark defaults as a starting point, but allow per-token override before publish.
- Keep typography mode-agnostic unless product requirements change.
Risks / constraints.
- The monorepo currently has no colour manipulation dependency; derivation logic must handle arbitrary CSS color strings safely.
- Derived palettes cannot guarantee WCAG 2.1 AA contrast; admin UX must surface contrast risk and encourage manual correction.
- Contract changes span theme types, Zod schemas, admin UI/i18n, API/OpenAPI payloads, and preset storage in
tenant_settings.
Creator / Marketplace Feature Proposals¶
Features below are proposals, not binding specs. Each entry follows the pattern: What → Why → Rough implementation pointers. Items are ordered by priority for a creator/marketplace LMS.
F1 — Course Completion Certificates (High)¶
What. An auto-generated, downloadable, shareable PDF certificate issued when enrollments.status transitions to completed. Each certificate has a unique verification URL so third parties (e.g., LinkedIn) can confirm authenticity.
Why. Learners in all verticals expect a credential artefact. For creators, certificates are the most-shared piece of content — every LinkedIn post carrying the certificate is free marketing.
Implementation pointers.
certificates(id, user_id, tenant_id, course_slug, issued_at, verification_code uuid)table. Unique index on(tenant_id, user_id, course_slug)— one certificate per completion.- PDF generation:
satori(renders JSX to SVG, then to PDF viasharp) or@react-pdf/renderer. Template reads from CMS tenant branding (logo, colours). - Issue trigger: inside the
active → completedtransition in the enrollment status machine (§3 of the enrollment spec). - Public verification route:
/learn/certificate/[code](unauthenticated, ISR). Shows learner name, course title, completion date, and a "verified" badge. - Add certificate download CTA to the learner dashboard and course completion page.
- i18n: certificate copy (learner name label, date format) must be in all six locale files.
F2 — Learner Reviews & Star Ratings (High)¶
What. Enrolled learners can submit a 1–5 star rating and optional text review after completing a course. The aggregate rating is displayed on the public course marketing page and the course catalog.
Why. Ratings are the core trust signal for marketplace buyers. Creators with high ratings convert browsers into purchasers; creators with low ratings get actionable signal. Without ratings, the marketplace is undifferentiated.
Implementation pointers.
course_reviews(id, user_id, tenant_id, course_slug, rating smallint check(rating between 1 and 5), body text, published boolean default true, created_at)table. Unique index on(tenant_id, user_id, course_slug)— one review per learner per course.- Review submission gated to
enrollments.status ∈ {'active','completed'}. - Aggregate:
SELECT AVG(rating), COUNT(*) FROM course_reviews WHERE tenant_id = ? AND course_slug = ? AND published = true— cache result with ISR tagcourse:<slug>. - Moderation:
tenant_admincan togglepublished = falseon a review via the admin roster page. - Display: star widget on the public course page; sort-by-rating filter in the course catalog (F3).
- WCAG: star inputs must be a radio group with a
<fieldset>+<legend>.
F3 — Course Discovery — Search & Filtering (High)¶
What. Full-text search across course titles and descriptions, with faceted filters for category, difficulty, price band (free/paid), language, and sort options (newest, highest rated, most enrolled).
Why. Discovery is the revenue engine of a marketplace. A flat course list does not scale past ~20 courses. Without search, creators investing in quality content get zero visibility advantage.
Implementation pointers.
- Route:
/learn/search?q=...&category=...&difficulty=...&free=1&sort=rating. - Server-side: PostgreSQL
tsvectorfull-text on acourse_catalogtable that syncs CMS metadata (slug, title, description, category, difficulty, price_cents, locale, published). Sync on the CMS webhook. Alternatively, proxy to the CMS search API if it exposes one. - All filter state lives in URL query params for shareability and SSR compatibility; no client-side store needed.
- Pagination: cursor-based (use
id > last_idnotOFFSET) to avoid page-drift as new courses are published. - i18n: search placeholder, filter labels, empty-state copy must be in all six locale files.
F4 — Creator / Instructor Public Profile (High)¶
What. A public-facing profile page per course author showing bio, avatar, social links, and their published course list with ratings and enrollment counts.
Why. Creator identity is the primary differentiation mechanism in a marketplace. Learners buy from instructors they recognise and trust; a profile page is the vehicle for building that trust and enabling return purchases.
Implementation pointers.
- Extend
userswithbio text, avatar_url text, social_links jsonb, instructor_slug text unique. - Route:
/learn/instructor/\[slug\](public, ISRrevalidate = 60). - Link CMS course authors to LMS users via a matching
emailfield — the CMS author email must equal the LMSusers.email. tenant_admincan designate which users have public profiles (not all users should be discoverable).- Profile avatar upload: pre-signed S3/R2 URL (same pattern as assignment file upload in F9).
F5 — Free Preview Entries (High)¶
What. Course authors mark specific CMS entries as preview: true. These entries are accessible without enrollment on the public course page — "try before you buy".
Why. The single biggest conversion lever for paid courses. Letting prospective learners experience the first lesson (or an especially compelling one) removes the purchase-risk objection.
Implementation pointers.
- Add
preview: booleanto the CMS Entry type (alongside_type,slug,title). Validated in the Zod Entry schema. getEntryAccess()returns{ mode: 'public-preview' }for these entries when the user is unauthenticated or un-enrolled.- In
public-previewmode: noentry_progresswrite, noquiz_attemptswrite, no sequential gating check. - Distinct from admin
?preview=1mode — the check is on the entry'spreviewflag, not the query param. - Render a
FreePreviewBannerabove the entry content with an enroll CTA. Banner is WCAG AA and translation-keyed. - The course marketing page lists preview-flagged entries with a "Preview" badge so learners know which to try.
F6 — Notification System (Medium)¶
What. Transactional in-app and email notifications: enrollment confirmation, course completion, certificate ready, new content added to an enrolled course, admin announcements.
Why. Keeps learners engaged after purchase; reduces inbound support requests; creates re-engagement opportunities ("your certificate is ready to download").
Implementation pointers.
notifications(id, user_id, tenant_id, type text, payload jsonb, read_at timestamptz, created_at)table.- In-app:
NotificationBellcomponent inSiteHeader; badge count via a lightweight RSC query; mark-read server action. - Email: extend the
sendEmail()interface shipped in Milestone 1 — Phase 3.5 with anotificationtemplate type. Use the recipient'susers.localeto select the translation. - Async dispatch: a Next.js route handler at
POST /api/internal/notifyhit by a simple DB-backed queue (one row per pending notification, polled by a cron job or triggered inline in server actions for low-latency events like enrollment confirmation). - Notification types to implement first:
enrollment.confirmed,course.completed,certificate.issued,course.content_updated.
F7 — Pricing Flexibility — Coupons & Discounts (Medium)¶
What. Course creators and tenant admins can create discount codes (flat amount off or percentage off), set time-bounded sale prices, and optionally scope codes to a specific course. Codes are applied at enrollment time.
Why. Discounts are the primary growth lever for creator marketplaces: launch promotions, seasonal sales, affiliate codes, and influencer partnerships all require this.
Implementation pointers.
discount_codes(id, tenant_id, course_slug nullable, code text unique, type enum('percent','flat'), value numeric, valid_from timestamptz, valid_until timestamptz, max_uses int nullable, used_count int default 0).- Apply in
enrollInCourseaction: validate code existence, expiry, remaining uses, and course scope before payment init. Decrementused_countatomically withUPDATE ... WHERE used_count < max_uses RETURNING id. - Store
applied_discount_code texton theenrollmentsrow for audit and reporting. - Admin UI: discount code CRUD under
/admin/courses/\[slug\]/pricing(tenant_admin and above). - Referral tracking variant (F13) piggybacks on the same code mechanism with a
refparameter.
F8 — Video Lesson Support (Medium)¶
What. A video CMS entry type with a playback URL (CDN-hosted HLS or MP4). The LMS tracks per-learner playback position and marks the entry complete when a configurable watch-threshold is reached (e.g., 90%).
Why. Video is the dominant content format for creator courses. Not supporting it excludes the majority of content creators.
Implementation pointers.
- Add
videoto the CMS Entry discriminated union:{ _type: 'video', slug, title, videoUrl, captionsUrl?, completionThreshold: number }. Validated in the Zod Entry schema. - Player:
<video>with HLS.js for adaptive streaming; native<track>for captions (WCAG 2.1 AA requires captions). - Progress persistence: add
video_position_seconds intcolumn toentry_progress. FirerecordVideoProgress({ entrySlug, positionSeconds })server action ontimeupdate, debounced to once per 5 s. - Completion: server action checks
positionSeconds / totalDurationSeconds >= completionThresholdand callsmarkEntryCompleteif threshold is reached. - No autoplay by default (WCAG 2.1 success criterion 1.4.2).
F9 — Assignment Submission System (Medium)¶
What. Full implementation of the assignment entry type stub. Learners upload files or submit text; instructors (course_admin) grade them and leave feedback; the grade is reflected in the learner's progress.
Why. Enables skill-based and professional courses (portfolios, projects, case studies) that go beyond quiz-style assessments. Critical for a creator marketplace serving professional audiences.
Implementation pointers.
assignment_submissions(id, user_id, tenant_id, course_slug, entry_slug, attempt_number, body text nullable, file_url text nullable, grade text nullable, feedback_text text nullable, graded_by bigint nullable references users(id), graded_at timestamptz nullable, submitted_at timestamptz).- File upload: pre-signed S3/R2 URL pattern — client requests a pre-signed PUT URL via a server action, uploads directly to object storage, posts the resulting URL back to
submitAssignment. submitAssignmentserver action: validates file URL scheme (must match trusted storage hostname), inserts submission row, writesentry_progressrow withcompleted_at = null(pending grade).gradeSubmissionserver action (course_admin / tenant_admin only): updatesgrade,feedback_text,graded_by,graded_at. Ifgradeis a pass, upsertsentry_progress.completed_at = now().- Admin UI: grading queue under
/admin/courses/\[slug\]/submissions.
F10 — Course Prerequisites (Medium)¶
What. A course can declare one or more prerequisite courses that a learner must have completed (enrollments.status = 'completed') before they can enroll.
Why. Enables structured learning series and certification tracks — the foundational value proposition of multi-course creator libraries.
Implementation pointers.
course_prerequisites(tenant_id, course_slug, requires_course_slug). Composite primary key(tenant_id, course_slug, requires_course_slug). No FK oncourse_slug— lives in CMS.- Check in
enrollInCourseaction after the published-course check: querycourse_prerequisitesfor the target course; for each prerequisite, verify the learner has acompletedenrollment. Reject with HTTP 422 + an array of unmet prerequisites. - Course marketing page: list prerequisites with completion status for authenticated users (enrolled-and-completed gets a checkmark; not-enrolled gets an enroll CTA).
tenant_adminsets prerequisites via/admin/courses/\[slug\]/settings.
F11 — Learning Paths / Course Bundles (Medium)¶
What. An ordered collection of courses sold or accessed as a unit. Learners enrolled in a path see a combined progress view and a single completion certificate.
Why. Increases revenue per learner (bundle pricing), enables credential tracks, and differentiates the platform from single-course competitors.
Implementation pointers.
learning_pathsCMS document type:{ slug, title, description, courseSlugOrder: string[] }.path_enrollments(id, user_id, tenant_id, path_slug, status, enrolled_at, completed_at)— mirrorsenrollmentsstructure. Enrolling in a path auto-enrolls the learner in all constituent courses (free) or initiates a single payment covering the bundle (paid).- Route:
/learn/path/\[slug\]— shows the path overview and per-course progress bars. - Path completion: triggered when all constituent
enrollments.status = 'completed'. Issues a separate certificate for the path. - Discount: path pricing can be lower than the sum of individual course prices (handled by F7 discount codes scoped to the path slug, or a dedicated
path_price_centsfield).
F12 — Student Q&A per Entry (Low)¶
What. A threaded Q&A section attached to each entry. Learners post questions; instructors (or other learners) answer; best answers are marked.
Why. Reduces instructor support burden; surfaces common confusions that inform content revisions; creates a sense of community that improves retention.
Implementation pointers.
entry_questions(id, user_id, tenant_id, course_slug, entry_slug, parent_id nullable references entry_questions(id), body text, upvotes int default 0, is_answered boolean default false, created_at).- Real-time not required — page refresh or optimistic mutation on post is sufficient for V1.
- Moderation:
course_adminandtenant_admincan delete or hide questions. Addhidden boolean default false. - Upvotes:
entry_question_upvotes(user_id, question_id)unique — prevents double-voting. - WCAG: Q&A list is a
<ul>with<li>items; the reply form announces submission via a polite live region.
F13 — Social Sharing & Referral Tracking (Low)¶
What. Share-on-LinkedIn and share-on-X buttons on certificate and course completion pages. UTM parameter preservation through the enrollment funnel for affiliate revenue attribution.
Why. Earned media is the primary organic acquisition channel for creator marketplaces. One share by a satisfied learner reaches hundreds of potential buyers. Referral tracking closes the attribution loop for affiliate partnerships.
Implementation pointers.
- UTM params (
utm_source,utm_medium,utm_campaign,ref) are captured in a short-lived (max-age=3600) first-party cookie at landing. enrollInCoursereads the cookie server-side and writesreferral_source jsonbto theenrollmentsrow. No third-party tracking service required.- Share buttons use platform deep-link URLs (e.g.,
https://www.linkedin.com/sharing/share-offsite/?url=...) — no SDK needed. Open intarget="_blank"withrel="noopener noreferrer". - Certificate share text: "I just completed (Course Title) on (Platform). Verify: /learn/certificate/(code)."
F14 — Subscription / All-Access Pass (Low)¶
What. An alternative to per-course pricing: a monthly or annual subscription granting access to all courses published on the tenant.
Why. Recurring revenue is more predictable than per-course sales; subscription models reduce purchase friction for learners who want to explore multiple topics.
Implementation pointers.
- Requires a real payment provider (blocked by the payment seam work and the Phase 6+ payment phase).
- Model subscriptions as a special
enrollmentwithcourse_slug = '*'and asubscription_expires_atcolumn. requireCourseAccess()checks for a wildcard enrollment before checking course-specific rows.- The existing
enrollmentsstatus machine still applies:activewhile subscription is current,cancelledon expiry or refund. - Subscription management UI under
/learn/account/subscription.
F15 — Offline Access / PWA Content Download (Low)¶
What. Learners can download course content for offline viewing. The existing PWA manifest supports installation; this feature adds service worker caching for static entry pages and lesson content.
Why. Mobile learners in bandwidth-constrained environments (travel, remote work) benefit significantly. Also a differentiator for corporate training deployments where connectivity is unreliable.
Implementation pointers.
- Service worker (Workbox): cache-first strategy for static entry RSC HTML; stale-while-revalidate for course home. Exclude per-learner routes (
/learn/dashboard, progress data). - "Download for offline" UI affordance on the course home page — triggers a service worker message to pre-cache all entries in the course.
- Storage quota warning: check
navigator.storage.estimate()before pre-caching; surface a warning if available storage is below a threshold (e.g., 50 MB). - Complex ISR interaction: downloaded content may be stale. Show a "Content updated — refresh to get the latest" banner when the service worker detects a new version.
- This feature has the highest implementation complexity in this list. Recommend deferring until all Phase 1–11 work is complete.
Documentation Backlog¶
- Documentation: Keep T-027 CMS/LMS drift matrix in sync as the CMS shadcn surface area or LMS dependency set drifts.
- Documentation: Expand
[reference/analytics.md](reference/analytics.md)with screenshots once admin dashboards ship. - Documentation: Promote any creator/marketplace feature above from proposal to a numbered phase in
[reference/implementation.md](reference/implementation.md)when it is committed to a milestone.
Summary¶
| Area | Item count | Highest-impact actions |
|---|---|---|
| Implementation plan follow-ups | 6 | Intra-course entry search, Phase 10.5 well-known files, Phase 11 e2e suites |
| Creator / marketplace proposals | 15 | F1 (certificates), F2 (reviews), F3 (search), F5 (free preview) |
This document tracks follow-ups and proposals only; shipped phase detail lives in the milestone planning docs (see Milestone 1 for Phase 3.5). Proposals are not binding; adoption should be prioritised against the current [reference/implementation.md](reference/implementation.md) phase plan.