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.mdbaseline;enginesis pinned to>=24.0.0). - npm 11 (workspaces; no Yarn / pnpm).
- SQLite for local development; PostgreSQL for production-like testing.
uvfor the Zensical docs build (docs:buildprovisions a virtualenv automatically on first run).
Install¶
From the repo root:
Run The App¶
From apps/lms:
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_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¶
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:3001before switching to tenant subdomains. E2E_REUSE_SERVERis 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 logJWTSessionError: 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 ideallynpm run verify) at the repo root before review. - Zero
anytypes: required across all TS/TSX. Useimport typefor type-only imports. - Per-component tests: every custom component under
src/components/**(excluding@open-learning-hub/uiprimitives) ships with a co-located test beside its source file (kebab-case.test.tsxper 031-file-naming.mdc). See.cursor/rules/030-testing.mdc. data-testidon every interactive element (buttons, links, inputs, dropdowns). Shared shadcn primitives are exempt — they exposedata-slot.- Server-only boundaries: never import Auth.js, Kysely,
@open-learning-hub/email, orsrc/lib/emailfrom a'use client'module. - Tenant scoping: every multi-tenant query MUST scope by
tenant_id; every per-user query MUST scope byuser_id. There is no declarative DB rule engine. - i18n: all user-facing copy lives in
messages/{locale}.jsonand is read viauseTranslations()(client) orgetTranslations()(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¶
- Author shared rules under the repository
.cursor/rules/directory, LMS-specific rules underapps/lms/.cursor/rules/, and architecture/reference content underapps/lms/docs/. npm run docs:syncstrips Cursor frontmatter, rewrites rule links, converts every.mdcrule to a.mdpage underdocs/zensical/docs-source/rules/, and mirrors the documentation sources. The converted pages are listed underProject Rulesand served at/docs/rules/. Never edit generateddocs-sourcefiles.npm run docs:buildruns Zensical and emitsdocs/zensical/site/.npm run docs:publishcopiessite/topublic/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.
Authoring links to dynamic-segment paths¶
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/[locale]/layout.tsx</code></a>. - Do not hand-edit
docs/zensical/docs-source/; it is regenerated on everydocs: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 testto exercise the sync-script unit tests, which cover this rewrite plus regressions for unaffected link shapes.