Skip to content

Developer Setup And Contributing

Audience: Developers contributing to the LMS.

Scope: Local setup, common commands, project layout, contribution expectations, and the docs pipeline.

Prerequisites

  • Node.js 24.x (matches the root AGENTS.md baseline; engines is pinned to >=24.0.0).
  • npm 11 (workspaces; no Yarn / pnpm).
  • SQLite for local development; PostgreSQL for production-like testing.
  • uv for the Zensical docs build (docs:build provisions a virtualenv automatically on first run).

Install

From the repo root:

npm install

Run The App

From apps/lms:

npm run dev

The dev server uses Next.js 16 with Turbopack on port 3001. The CMS app runs on port 3000 under apps/cms.

predev runs npm run docs:publish so the local dev build always serves a fresh static docs site under /docs/.

Environment

Copy .env.local.template to .env.local and fill in the required keys. Validated server-side by src/lib/env.ts:

Group Variables
Auth AUTH_SECRET, AUTH_URL, AUTH_TRUST_HOST, AUTH_GOOGLE_ID?, AUTH_GOOGLE_SECRET?
Database DATABASE_URL (e.g. sqlite:./data/dev.sqlite locally, postgres://... in prod)
CMS CMS_API_URL, CMS_PROJECT_UUID, CMS_API_UUID, CMS_WEBHOOK_SECRET
Email EMAIL_FROM, RESEND_API_KEY, SMTP_HOST?, SMTP_PORT?, SMTP_USER?, SMTP_PASSWORD?, APP_BASE_DOMAIN
Rate-limit RATE_LIMIT_DRIVER (memory for dev/test, upstash for prod), UPSTASH_REDIS_REST_URL?, UPSTASH_REDIS_REST_TOKEN?

Server-side variables MUST NOT use the NEXT_PUBLIC_ prefix. See admin/security-and-rate-limiting.md for production guidance.

Email delivery in local development

Leave RESEND_API_KEY and SMTP_HOST unset to use the non-production console driver. It logs the full recipient and token-bearing links to the local server terminal, allowing verification, reset, invitation, and magic-link flows without external delivery.

Resend takes precedence over SMTP when both are configured. Restart the dev server after changing email environment variables because the client is cached in-process. Do not commit .env.local or share console-driver token links.

Provider setup, local SMTP options, production requirements, and the enumeration-safe troubleshooting checklist are canonical in Email Delivery. Auth routes and token behavior remain in Authentication.

Database Commands

npm run db:migrate
npm run db:reset
npm run db:seed

db:reset is destructive: it rolls back all migrations, reapplies them, and regenerates public/openapi.yaml. It does not seed demo data.

After seeding, see Development seed data for fixture roles, tenants, env-driven password configuration, and sign-in hosts. Concrete account identifiers live in scripts/db-seed.ts.

Use DATA_DIR=./data/test for isolated end-to-end test state. Do not reuse the default tenant/course names in tests; use unique names to make failures easier to diagnose.

Migrations live under src/db/migrations/; the canonical Kysely Database interface is src/db/types.ts. See development/database-schema.md.

Quality Commands

For root scripts/ command intent and flags, use the canonical reference: Root Scripts Reference.

Command Purpose
npm run check Biome check with auto-fixes (TS/JS/CSS/JSON).
npm run lint Biome lint check only.
npm run format:md Prettier formatting for *.md, *.mdx, *.mdc only.
npm run typecheck tsc --noEmit.
npm run test Vitest one-shot run.
npm run test:unit Unit tests under tests/unit/.
npm run test:components Co-located component tests under src/components/**.
npm run db:reset Roll back all migrations, re-migrate, and regenerate OpenAPI (destructive).
npm run test:e2e:prepare Reset data/test, migrate, and seed demo users for Playwright.
npm run test:e2e Playwright (loads .env.test, runs test:e2e:prepare via global setup).
npm run openapi:generate Emit public/openapi.yaml from Zod schemas.
npm run docs:sync Mirror rules + docs into docs/zensical/docs-source/.
npm run docs:build Build the Zensical static site into docs/zensical/site/.
npm run docs:publish Run docs:build and copy site/public/docs/.
npm run docs:check Build + validate the Zensical docs site.
npm run verify check + openapi:generate + test:components + build + docs:check.

Playwright e2e notes:

  • Impersonation specs require apex sign-in on http://lvh.me:3001 before switching to tenant subdomains.
  • E2E_REUSE_SERVER is opt-in (E2E_REUSE_SERVER=true npm run test:e2e). By default, Playwright starts isolated servers with pinned test env.
  • If you reuse a server started with a different AUTH_SECRET, Auth.js may log JWTSessionError: no matching decryption secret.

After substantive changes run npm run check from the repo root (Turborepo runs both apps and shared packages).

Project Layout

apps/lms/
├── src/
│   ├── app/\[locale\]/          # App Router (main, lms, admin, blog, invite, _suspended)
│   ├── auth/                  # Auth.js v5 config, credentials, oauth, guards, actions
│   ├── components/            # Feature components + co-located tests (import UI from @open-learning-hub/ui)
│   ├── db/
│   │   ├── client.ts          # Kysely dialect picker (PG vs SQLite)
│   │   ├── types.ts           # Handwritten Database interface — source of truth
│   │   ├── migrations/        # Numbered migrations (Kysely Migrator)
│   │   └── queries/           # Per-domain query helpers
│   ├── hooks/                 # Custom React hooks
│   ├── i18n/                  # next-intl bootstrap
│   ├── lib/
│   │   ├── auth/              # Auth helpers shared with non-Auth.js callers
│   │   ├── cms/               # Typed fetch + asset proxy for the headless CMS
│   │   ├── email/             # LMS env/i18n adapters for @open-learning-hub/email
│   │   ├── log.ts             # Structured logger with redaction
│   │   ├── rate-limit/        # Driver abstraction (memory + Upstash)
│   │   └── tenant/            # Host → tenant resolution
│   ├── proxy.ts               # Next.js 16 proxy (locale, tenant, auth gates, headers)
│   ├── schemas/               # Zod schemas + zod-to-openapi registry
│   └── types/                 # Shared types (incl. next-auth.d.ts augment)
├── tests/
│   ├── setup.ts               # Vitest global setup (jest-dom, polyfills)
│   ├── unit/                  # Lib + schema + db query tests
│   ├── e2e/                   # Playwright (Phase 11+)
│   └── utils/render-with-theme.tsx
├── messages/{en,es,fr,de,pt,zh}.json
├── public/
├── scripts/
│   ├── db-migrate.ts
│   ├── db-seed.ts
│   └── openapi-generate.ts
└── docs/                      # This documentation tree (source of truth)

Contribution Expectations

  • Validation gate: every change runs npm run check (and ideally npm run verify) at the repo root before review.
  • Zero any types: required across all TS/TSX. Use import type for type-only imports.
  • Per-component tests: every custom component under src/components/** (excluding @open-learning-hub/ui primitives) ships with a co-located test beside its source file (kebab-case.test.tsx per 031-file-naming.mdc). See .cursor/rules/030-testing.mdc.
  • data-testid on every interactive element (buttons, links, inputs, dropdowns). Shared shadcn primitives are exempt — they expose data-slot.
  • Server-only boundaries: never import Auth.js, Kysely, @open-learning-hub/email, or src/lib/email from a 'use client' module.
  • Tenant scoping: every multi-tenant query MUST scope by tenant_id; every per-user query MUST scope by user_id. There is no declarative DB rule engine.
  • i18n: all user-facing copy lives in messages/{locale}.json and is read via useTranslations() (client) or getTranslations() (server). Add keys to all six locale files in the same change.
  • Commits: never commit code directly; wait for the user to request a commit.

Documentation Pipeline

  1. Author shared rules under the repository .cursor/rules/ directory, LMS-specific rules under apps/lms/.cursor/rules/, and architecture/reference content under apps/lms/docs/.
  2. npm run docs:sync strips Cursor frontmatter, rewrites rule links, converts every .mdc rule to a .md page under docs/zensical/docs-source/rules/, and mirrors the documentation sources. The converted pages are listed under Project Rules and served at /docs/rules/. Never edit generated docs-source files.
  3. npm run docs:build runs Zensical and emits docs/zensical/site/.
  4. npm run docs:publish copies site/ to public/docs/ so Next.js serves them locally and in production.

Every docs/reference/*.md file and every converted rule target must appear in docs/zensical/zensical.toml. Sync tests enforce source-to-navigation parity. Within every top-level navigation section, recursively keep sibling entries in case-insensitive natural alphanumeric order by displayed label.

For the doc-type taxonomy and which file goes where, see docs/index.md.

Zensical's link validator forbids nested square brackets in link text, so a naive [`src/app/\[locale\]/layout.tsx`](github-url) raises an unresolved link reference warning. To keep author markdown readable, the sync script (docs/zensical/scripts/sync_docs.py) detects this exact pattern when the URL points at the repository on GitHub and rewrites it to inline HTML before Zensical sees it. The rendered site shows the brackets verbatim and the GitHub URL is preserved.

Authoring guidance:

  • Write source markdown as usual: <a href="https://github.com/open-learning-hub/learning-platform/blob/main/apps/lms/src/app/%5Blocale%5D/layout.tsx"><code>src/app/&#91;locale&#93;/layout.tsx</code></a>.
  • Do not hand-edit docs/zensical/docs-source/; it is regenerated on every docs:sync.
  • For new links that do not need to expose the dynamic segment in the label, prefer descriptive link text (the M2 milestone uses the pattern "the admin route segment" with the path in backticks beside the link).
  • Run make -C docs/zensical test to exercise the sync-script unit tests, which cover this rewrite plus regressions for unaffected link shapes.