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*.mdcin each app (format:md/format:checkscripts) - 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 sharedauth-utils,platform-config, andserverpackages
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 |
@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:sync → docs-source/; docs:build → site/; docs:publish → public/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.localeafter sign-in, falling back to the tenant default and thenen. - Public pages use
next-intlheader negotiation and the tenant default when no explicit locale is present. src/lib/cms/client.tsaccepts alocaleparameter 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}.jsonand must exist in all supported locale files.