Skip to content

Tech stack (LMS app)

The LMS is a statically generated, multi-tenant Learning Management System (Next.js 16, edX CMS-backed content). This document splits shared tooling, LMS-only dependencies, and CMS-only authoring dependencies so the table matches each app’s package.json.

For a concise CMS vs LMS matrix, see T-027 CMS vs LMS Drift. Monorepo-wide baselines: AGENTS.md.

Summary (LMS)

Framework Next.js 16 (App Router)
UI React 19, shadcn/ui (new-york), Tailwind CSS v4
Content edX CMS over HTTP (src/lib/cms/), ISR revalidate 60s where applicable
Auth Auth.js (NextAuth v5), Credentials provider
Data Kysely, PostgreSQL (SQLite in local dev)
Schemas Zod at boundaries; OpenAPI generation via dev tooling
Language TypeScript (strict)

Shared monorepo baseline (both apps)

Authoritative list: AGENTS.md Baselines. In short:

  • Node ≥ 24, npm 11, Turborepo
  • Next.js 16 (App Router, next dev --turbopack), React 19, TypeScript strict
  • Tailwind CSS v4 (no tailwind.config.ts), shadcn/ui v4 (new-york)
  • Biome for TS/JS/CSS/JSON; Prettier for *.md, *.mdx, and *.mdc in each app (format:md / format:check scripts)
  • Vitest + happy-dom; Playwright e2e is wired in both apps
  • Zod v4 (remember z.record(z.string(), z.unknown()) two-arg form)
  • Workspace packages include @open-learning-hub/design-tokens, email, portable-text, ui, widget-renderers, widget-wire-schemas, plus shared auth-utils, platform-config, and server packages

LMS-only (apps/lms dependencies)

Rows reflect apps/lms/package.json dependencies and notable devDependencies.

Layer Technology Notes
Framework Next.js App Router next build for production
UI React, shadcn/ui via @open-learning-hub/ui, Tailwind CSS v4 LMS consumes shared package primitives; CMS keeps a larger local set — see T-027 CMS vs LMS Drift
Icons Lucide (lucide-react) 035-icons-lucide.mdc
Rich text (render) @portabletext/react Rendering only; no Portable Text editor stack in LMS
Portable Text (shared) @open-learning-hub/portable-text Kernel + adapters compiled from source
Widgets @open-learning-hub/widget-renderers, @open-learning-hub/widget-wire-schemas Preview parity with CMS
Design tokens @open-learning-hub/design-tokens Imported in globals.css; LMS-specific @source and base-layer tweaks
Auth Auth.js / NextAuth v5 Credentials provider; session strategy per src/auth.ts
Passwords bcryptjs Server-side hashing
Email @open-learning-hub/email, React Email, Resend, Nodemailer Shared server-only render/delivery package; LMS owns environment, i18n, event logic, preferences, and persistence. See Email System.
Data access Kysely Type-safe SQL, no ORM codegen
Database drivers pg, better-sqlite3 PostgreSQL in prod; SQLite in dev
Forms react-hook-form + @hookform/resolvers Zod resolvers
i18n next-intl Locales: en, es, fr, de, pt, zh
Theming next-themes Light / dark / system; defaultTheme="dark" in locale layout; default localStorage key theme
Carousel embla-carousel-react Where used in UI
Toasts sonner
Sanitization sanitize-html Server-side sanitization of Portable Text rawHtml blocks at the CMS fetch boundary (src/lib/cms/sanitize-cms-html.ts)
API docs (dev) @asteasolutions/zod-to-openapi + swagger-ui-react devDependencies; npm run openapi:generate
Server boundary server-only Keep secrets and DB off the client
Utilities uuid, yaml, clsx, tailwind-merge, class-variance-authority As imported in app code
Testing Vitest, @testing-library/react, happy-dom test, test:unit, test:components
Lint & format Biome + Prettier Same split as CMS for prose extensions
Scripting tsx Migrations, seeds, OpenAPI (root / app scripts)
Documentation site Zensical + uv apps/lms/docs/zensical: docs:syncdocs-source/; docs:buildsite/; docs:publishpublic/docs/ (Next static assets; predev runs publish)

CMS-only (authoring app — not LMS runtime)

These appear in apps/cms/package.json but not as LMS learner dependencies. They power the workarea, export, and authoring experiences.

Layer Technology Role
Portable Text authoring @portabletext/editor, @portabletext/toolbar, @portabletext/toolkit, @portabletext/to-html, @portabletext/html, @portabletext/keyboard-shortcuts Editor + serialization
Authoring UX @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities Drag-and-drop
Layout react-resizable-panels Resizable workarea panes
State zustand Client workarea state
Markdown / export marked, fast-xml-parser, fflate Export and static asset paths
JSON inspection @uiw/react-json-view Admin/diagnostics UI
Test DOM jsdom CMS test environment
E2E @playwright/test CMS Playwright suite

Locale threading

The learner’s resolved locale is passed to all CMS client calls as ?locale=<locale>.

  • Authenticated LMS routes resolve locale from users.locale after sign-in, falling back to the tenant default and then en.
  • Public pages use next-intl header negotiation and the tenant default when no explicit locale is present.
  • src/lib/cms/client.ts accepts a locale parameter on every content method and appends it to the CMS query string.
  • CMS-authored copy is rendered as returned. If the CMS returns content in only one language, the LMS does not machine-translate or apply fallback translations to that copy.
  • LMS-owned UI chrome, errors, form labels, metadata, and email copy still come from messages/{locale}.json and must exist in all supported locale files.