Skip to content

Maintain CMS and LMS drift matrix

Problem

The CMS and LMS intentionally share a platform baseline while retaining different product responsibilities, dependencies, and runtime behavior. Those differences need one maintained contract so contributors can distinguish intentional divergence from accidental drift.

The canonical shared stack lives in the root AGENTS.md Baselines. Its Workspace dependencies section explains hoisted development tooling versus per-app runtime dependencies. The matrix below records divergences only.

Area CMS (apps/cms) LMS (apps/lms) Notes
Role Authoring (edx-create), workarea UI Multi-tenant learner + public marketing site + admin Ports 3000 / 3001
Design tokens Imports @open-learning-hub/design-tokens; globals.css adds authoring-only @theme brand keys (--color-exe-*) and @source for widget renderers Imports design tokens; globals.css adds @source for site chrome + widget renderers and minimal base layer (for example font-sans) Semantic --success / --warning / --info live in packages/design-tokens
shadcn UI primitives Workspace package @open-learning-hub/ui (same primitive source as LMS); app-local src/components/ui/ contains CMS composites like file-drop-zone, portable-text-editor, pagination Workspace package @open-learning-hub/ui; LMS has no local src/components/ui/ primitive tree Shared primitives (button, card, dialog, tabs) stay aligned with apps/cms/.cursor/rules/cms-system.mdc
Portable Text Full editor stack (@portabletext/editor, toolbar, toolkit, keyboard-shortcuts, to-html, html) @portabletext/react for rendering only LMS does not ship the authoring editor
Authoring UX @dnd-kit/* + react-resizable-panels for drag/resize workarea No @dnd-kit/*; no LMS usage of react-resizable-panels in learner/admin chrome LMS uses zustand for course-sidebar expansion state (src/lib/store/sidebar.ts)
Auth / learner DB Auth.js v5 + Kysely for CMS API/admin model; password helpers re-exported from @open-learning-hub/auth-utils Auth.js v5 + Kysely for tenant-scoped LMS data; server-only boundaries Both apps have independent schemas/migrations and DB scripts
Sign-out confirmation UX Branded /logout page, guarded redirect to / when unauthenticated, CTA set: sign out / back to workarea / cancel home; pages.signOut maps Auth.js fallback to this route Branded /learn/sign-out page, guarded redirect to / when unauthenticated, CTA set: sign out / back to dashboard / cancel home; pages.signOut maps Auth.js fallback to this route Parity achieved for branded sign-out confirmation patterns while preserving app-specific route naming (/logout vs /learn/sign-out).
Password policy newPasswordSchema from @open-learning-hub/auth-utils/password-policy for reset + admin create; sign-in remains non-empty + bcrypt verify newPasswordSchema via src/schemas/auth.ts for sign-up/reset/invite/account set-change; sign-in remains non-empty + bcrypt verify Canonical policy: min 8, max 128, requires uppercase + lowercase + digit. Dev/e2e passwords resolve from SEED_DEMO_PASSWORD via @open-learning-hub/auth-utils/seed-demo-password (T-028)
Structured logger src/lib/log.ts with import "server-only" src/lib/log.ts with import "@/lib/server-only" Byte-identical except the server-only import line; canonical contract: 091-structured-logging.mdc
Email App-local CMS events, i18n templates, preferences, and email_events schema App-local LMS events, i18n templates, preferences, and email_events schema Both use server-only @open-learning-hub/email for React Email rendering and Resend/SMTP/console delivery; runtime event persistence is not wired yet
CMS content API Implements and serves CMS APIs src/lib/cms/ HTTP client to edX CMS LMS is a consumer
OpenAPI openapi:generate via src/lib/api/generate-openapi.ts openapi:generate via scripts/openapi-generate.ts + @asteasolutions/zod-to-openapi in devDependencies
Markdown / XML / export marked, fast-xml-parser, fflate, jsdom, @uiw/react-json-view Not in LMS runtime dependency set CMS export/import and diagnostics tooling
E2E Playwright suite (test:e2e) Playwright suite (test:e2e) under tests/e2e/ Both apps run e2e via workspace scripts
Docs site Zensical under apps/cms/docs/zensical Zensical under apps/lms/docs/zensical: docs:sync mirrors into docs-source/, docs:build emits site/, docs:publish copies site/ to public/docs/ LMS mirror script: apps/lms/docs/zensical/scripts/sync_docs.py
Public docs nav link shared-site-header-nav-docs in authenticated site header (Button + external <a href="/docs/">) main-nav-link-docs in MainNav when site hierarchy contains a docs node (text link + ExternalLink, same URL/new-tab behavior) Shared contract: i18n nav.docs, /docs/, target="_blank", rel="noopener noreferrer"
Admin nav link shared-site-header-nav-admin when JWT roles[] includes admin, super-admin, or tenant-admin main-nav-link-admin only when CMS hierarchy includes admin and session role is tenant_admin, course_admin, or super_admin Both link to /admin; role naming intentionally differs (CMS hyphen slugs vs LMS underscore enums)
LMS components widgets Catalogs lms-featured-courses / lms-blog-listing / lms-course-catalog; CMS surfaces render LmsComponentPlaceholderWidget Overrides those three types on SharedWidgetView with live section renderers fed by resolveLmsWidgetData + ResolvedLmsWidgetDataProvider Zero-config (empty content schema); see apps/lms/docs/guides/content-rendering.md
Lint/format Biome + Prettier on *.{md,mdx,mdc} Same pattern No .prettierrc; .prettierignore only
Component filenames kebab-case (theme-provider.tsx) under src/components/; tests commonly in __tests__/ with matching kebab names kebab-case under src/components/ and src/app/**/_components/; co-located kebab-case.test.tsx Canonical policy: 031-file-naming.mdc; exports stay PascalCase
Module / lib filenames kebab-case (feature-flag-registry.ts, data-dir.ts) under src/lib/**, src/auth/**, and peers kebab-case across src/lib/**, src/auth/**, src/db/**, src/types/**, and route-colocated action modules CMS minor outlier: tokenVersion.ts in src/lib/auth/; Next.js bracket params remain framework-standard
Test harness __tests__/ + kebab names beside source tests/utils/render-with-theme.tsx (export renderWithTheme); remote unit tests under tests/unit/** use kebab-case.test.ts patterns LMS details in apps/lms/.cursor/rules/030-testing.mdc

Both apps use Lucide only; the canonical policy is 035-icons-lucide.mdc.

Both apps use Next.js 16 proxy.ts, not middleware.ts, for cross-cutting request handling.

CMS and LMS both use next-themes with attribute="class", defaultTheme="dark", enableSystem, disableTransitionOnChange, and the default storage key theme. This shared app chrome behavior is parity, not drift.

Proposal

  • Keep this matrix as the canonical cross-app divergence record.
  • Update it in the same change whenever a shared dependency, UI primitive, runtime contract, or app-specific implementation changes.
  • Link to the canonical shared rule or implementation rather than duplicating policy in this ticket.
  • Treat undocumented differences found during review as drift until they are aligned or recorded here as intentional.

Acceptance criteria

  • The matrix reflects current CMS and LMS dependencies and behavior.
  • Every intentional divergence names its owning implementation or canonical policy where useful.
  • Shared contracts are described as parity rather than duplicated as app-specific rules.
  • Changes affecting both apps update this ticket in the same pull request.
  • Both CMS and LMS documentation sites expose this ticket under Platform.
  • npm run check passes.

Out of scope

  • Forcing CMS and LMS to use identical mutation transports, dependencies, or product features.
  • Replacing the canonical root and app-specific AGENTS.md files.
  • Tracking implementation work that belongs in a narrower audit ticket.

Notes / decisions log

  • 2026-07-16: Converted the living drift reference into an in-progress platform ticket while preserving the comparison matrix.
  • 2026-07-17: Aligned CMS and LMS new-password validation to shared @open-learning-hub/auth-utils/password-policy and standardized seed/e2e credentials to a shared seed password literal (no backward-compatibility dataset support).
  • 2026-07-17: Opened T-028 to replace hardcoded seed/e2e/test password literals with SEED_DEMO_PASSWORD across both apps.
  • 2026-07-31: Aligned LMS and CMS branded sign-out confirmation flows (guard behavior, CTA set, Auth.js pages.signOut wiring, and auth-page test hooks).