Imported from Gopendranath/lms-monorepo-v2 (
AGENTS.md). Install upstream withnpx skills add Gopendranath/lms-monorepo-v2. Copyright stays with the author.
AGENTS.md — lms-monorepo-v2
Global coding conventions, architecture, and access model for the LMS monorepo.
Monorepo Overview
lms-monorepo-v2/
├── apps/
│ ├── server/ # Express 5 REST API (port 4000)
│ ├── admin-web/ # Next.js 16 admin dashboard (port 3003)
│ ├── lms-web/ # Next.js 16 LMS frontend (port 3002)
│ ├── web/ # Next.js 16 public site (port 3000)
│ └── docs/ # Next.js 16 documentation site (port 3001)
├── packages/
│ ├── db/ # Prisma 7 ORM + PostgreSQL
│ ├── types/ # Shared Zod v4 schemas + TypeScript types
│ ├── redis/ # ioredis caching client
│ ├── queue/ # BullMQ background job queue
│ ├── object/ # Cloudflare R2 S3-compatible storage
│ ├── stripe/ # Stripe v22 payment processing
│ ├── ui/ # shadcn/ui React components + Tailwind v4
│ ├── eslint-config/ # Shared ESLint flat config presets
│ └── typescript-config/ # Shared tsconfig presets
├── turbo.json # Turborepo task orchestration
├── pnpm-workspace.yaml
└── package.json
Tooling: Turborepo v2, pnpm 11, TypeScript 5.9, Node >=18.
Role Model
There are exactly 4 roles. No "instructor" role exists.
| Role | Slug | Access Level |
|---|---|---|
| Super Admin | superadmin |
Full access to everything. Bypasses all permission checks. |
| Admin | admin |
Manages users, courses, content, enrollments, platform data. |
| Maintainer | maintainer |
Limited operational access (bug reports, video timestamps). |
| User | user |
Standard platform user. No administrative access. |
Defined in:
packages/types/src/schemas/user.ts—userRolesconst,UserRoletype, Zod schemasapps/server/src/lib/permissions.ts— Better AuthcreateAccessControlrole definitionsapps/admin-web/src/lib/permissions.ts— Display-only metadata for the permission UI
Role Permission Definitions (server-side)
// apps/server/src/lib/permissions.ts
superadmin: full CRUD on all resources (user, session, admin, maintainer,
course, content, enrollment, system, audit-log, bug-report, video-timestamp)
admin: all defaultStatements from better-auth admin plugin
(no explicit user/session statements)
maintainer: bug-report: ["create"]
video-timestamp: ["edit"]
user: user: []
App Access Model
admin-web (port 3003)
Accessible by: superadmin, admin only.
maintaineranduserroles are blocked from accessing admin-web entirely.- Authentication proxy in
src/proxy.tschecks for a valid session cookie. - Server-side permission checks (
requirePermissionmiddleware) enforce per-resource access. - Superadmin-only pages (Permissions management) use
requireSuperadminmiddleware. - The sidebar conditionally shows the Permissions link via
useCanManagePermissions().
lms-web (port 3002)
Accessible by: superadmin, admin, maintainer, user.
- All authenticated roles can access lms-web.
- Some routes may be public (unauthenticated).
- Role-based UI conditional rendering is handled client-side.
web (port 3000)
Public-facing site. No authentication required.
docs (port 3001)
Documentation site. No authentication required.
Permission System
Architecture
Permissions are server-driven and checked at the API layer:
- Better Auth
createAccessControldefines allowed statements per role inapps/server/src/lib/permissions.ts. - Per-user overrides are stored in the
UserPermissionDB table (prisma/auth.prisma) and cached in-memory with a 60s TTL (apps/server/src/lib/permissions-cache.ts). requirePermission(resource, action)middleware checks the user's role + per-user overrides before allowing access.requireSuperadminmiddleware is a hardened guard for meta operations (permission management) that bypasses the DB permission table.- The admin-web UI renders checkboxes using metadata from
apps/admin-web/src/lib/permissions.ts.
Resources & Actions
| Resource | Actions |
|---|---|
user |
create, list, get, update, delete, set-role, ban, impersonate, set-password, set-email |
session |
list, revoke, delete |
admin |
create, delete, list |
maintainer |
assign, revoke |
course |
create, read, update, delete, publish, archive |
content |
create, read, update, delete, publish |
enrollment |
create, read, update, delete, approve |
system |
read, update |
audit-log |
read, export |
bug-report |
create, read |
video-timestamp |
edit |
Technology Stack
Server
- Runtime: Express 5, TypeScript (ESM,
"module": "NodeNext") - Auth: Better Auth v1.6 (email/password + GitHub OAuth), Prisma adapter
- Database: Prisma 7 + PostgreSQL (
@prisma/adapter-pg) - Validation: Zod v4 (schemas in
@lms/types) - Logging: Pino structured logger + pino-http
- Background Jobs: BullMQ (Redis-backed via
@lms/queue) - Cache: ioredis (via
@lms/redis) - Storage: Cloudflare R2 (AWS SDK v3, via
@lms/object) - Payments: Stripe v22 (via
@lms/stripe)
Frontends
- Framework: Next.js 16 (App Router), React 19
- UI Library: shadcn/ui (base-nova style) via
@lms/ui - Styling: Tailwind CSS v4 + PostCSS
- State/Data: TanStack React Query v5 (admin-web, lms-web), Zustand (lms-web)
- Auth Client: better-auth client
- Icons: lucide-react
- Animations: motion (Framer Motion)
- Notifications: sonner
Coding Conventions
General Rules
- TypeScript strict mode — no
any. Useunknown+ type guards. - Named exports — default exports only for Express app instance.
- No barrel files (
index.ts) in leaf directories (controllers, services, middleware, components). - Kebab-case file naming:
user.controller.ts,not-found.ts. - Type-only imports:
import type { Request } from "express". .jsextension in imports for server (required by"module": "NodeNext").
Server Architecture (Express 5)
src/
├── index.ts # Entry: validate env, connect services, start server
├── app.ts # Express app factory (middleware → routes → error handler)
├── config/env.ts # Zod-validated env vars (fail-fast)
├── middleware/ # Auth, validation, error handling
├── routes/ # Route definitions (mount controllers + middleware)
├── controllers/ # Thin: parse input → delegate to service → send response
├── services/ # Fat: business logic + Prisma queries
├── lib/ # Auth setup, error classes, utilities
├── types/ # Express type augmentations
└── workers/ # BullMQ background job processors
- One file per concern — never mix routes, controllers, services.
- Thin controllers, fat services — controllers only parse input/output. All logic in services.
- Async handler wrapper — never write
try-catchin route handlers. UseasyncHandlerto forward errors to centralized error handler. - Centralized error handler — 4-arg middleware catching all
next(err)calls. Returns{ success: false, error: { message } }.
Response Envelope
// Success
{ success: true, data: { ... } }
// Error
{ success: false, error: { message: "..." } }
Middleware Registration Order
Security (cors) → Parsing (json) → Logging (pino-http) → Auth → Routes → 404 → Error Handler
Next.js Frontend Architecture
src/
├── app/ # App Router pages (layout.tsx, page.tsx, loading.tsx, error.tsx)
├── components/ # Reusable React components
├── hooks/ # Custom hooks (TanStack Query hooks, auth hooks)
├── lib/ # API clients, query provider, auth client, utilities
└── store/ # Zustand stores (lms-web)
- Default to Server Components — only use
"use client"when browser APIs, hooks, or event handlers are needed. - Keep Client Components as leaves in the component tree.
- Fetch data in Server Components — not in
useEffect. Use TanStack Query for client-side mutations and stale-while-revalidate. - API proxy via
next.config.tsrewrites:/api/auth/*and/api/v1/*→ backend.
Environment Variables
- Validate all env vars at startup using Zod. Fail immediately if missing.
- Access via validated
envobject, neverprocess.envdirectly after startup. - Each app/package has its own
.env.example.
Error Handling
- Custom error classes with HTTP status codes (
AppError,NotFoundError,ValidationError,UnauthorizedError,ForbiddenError). - Never throw raw strings or plain
Error. - Per-package error handlers normalize errors from Prisma, Redis, S3/R2, Stripe, Zod into
{ statusCode, message }format.
Database
- Single
PrismaClientsingleton via@lms/db/src/index.ts. - Schema split:
auth.prisma(User, Session, Account, Verification, UserPermission) +domain.prisma(Course, Section, Content, Enrollment, Payment, AuditLog, etc.). - Prisma queries live in services, never in controllers.
Shared Packages
@lms/types— Zod schemas + types, foundation package depended on bydb,redis,stripe,object.@lms/db— Prisma client + error handler.@lms/redis— ioredis singleton + error handler.@lms/queue— BullMQ factory functions (createQueue,createWorker), depends on@lms/redis.@lms/object— S3/R2 client + presigner + error handler.@lms/stripe— Stripe client + error handler.@lms/ui— 62 shadcn/ui components, hooks, Tailwind globals.- Use workspace protocol (
workspace:*) for all cross-package imports. - No app-specific code in packages. Packages must stay app-agnostic.
Git & Commits
pnpm run verifybefore pushing (check-types + lint + build).- Commit messages match repo style (concise, imperative mood).
- No commits without explicit user request.
Do's
- Validate every Zod schema at the boundary (route/controller level).
- Use
revalidatePath/revalidateTagafter mutations in server actions. - Use
<Image>fromnext/imagewith explicit dimensions. - Use
<Link>fromnext/linkfor internal navigation. - Set
output: "standalone"only when required by deployment target. - Keep third-party scripts out of critical path via
next/script. - Graceful shutdown: handle
SIGTERM/SIGINTto close HTTP server, Prisma, Redis.
Don'ts
- Don't use
console.log— use Pino (server) or proper logging (frontend). - Don't put business logic in route handlers or controllers.
- Don't throw raw errors — always use custom
AppErrorsubclasses. - Don't use
any— preferunknownwith type guards. - Don't hardcode config — all config comes from Zod-validated env vars.
- Don't access
process.envoutsideconfig/env.tson the server. - Don't call
next()after sending a response. - Don't start the server without validating DB/Redis/R2/Stripe connections.
- Don't import app-specific code from another app.
- Don't commit
.envfiles or secrets.
graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
When the user types /graphify, use the installed graphify skill or instructions before doing anything else.
Rules:
- For codebase questions, first run
graphify query "<question>"when graphify-out/graph.json exists. Usegraphify path "<A>" "<B>"for relationships andgraphify explain "<concept>"for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run
graphify update .to keep the graph current (AST-only, no API cost).