Multi-tenant Learning Management System backend built with Express.js 5, Prisma (PostgreSQL), Redis, Stripe, and JWT auth. Supports SCORM/AICC/xAPI courses, licensing, company (B2B) purchases, certificates, quizzes, and more.
- Overview
- Tech Stack
- Architecture Concepts
- Project Structure
- Quick Start
- Environment Variables
- Available Scripts
- API Conventions
- Modules / Feature Map
- Authentication & Authorization
- Postman Collection
- Further Documentation
DIEGO is a multi-tenant LMS platform. A single backend instance serves multiple Tenants (white-labeled platforms), each of which can have their own courses, licensees, employees, and branding (subdomain / custom domain / logo / primary color).
Core domains modeled in the system:
- Users & Auth โ multi-level RBAC (
PRIVATE_USER,COMPANY_ADMIN,COMPANY_EMPLOYEE,LICENSEE,PLATFORM_ADMIN,TUTOR,TEACHER) - Courses & Lessons โ SCORM, AICC, xAPI, PDF, video, quizzes, checklists, LTI, slide decks
- Enrollments & Progress โ lesson progress, SCORM session tracking, anti-cheat logging, auto-completion โ certificate generation
- Commerce โ single course purchase, packages, licenses, company bulk purchases, coupons, Stripe payments, invoices
- Licensing โ licensees run their own branded course catalog under a tenant, earn a revenue share (
LicenseeIncome) - Certificates โ auto-issued PDF certificates with QR code + timestamp proof
- Support & Notifications โ support tickets, in-app notifications, severity alerts
- i18n โ most user-facing content (titles, descriptions, messages) stored as
Jsonto supportit,en,fr,zh
| Layer | Technology |
|---|---|
| Runtime | Node.js 18+ |
| Framework | Express 5 |
| ORM | Prisma 5 (@prisma/client, @prisma/adapter-pg) |
| Database | PostgreSQL |
| Cache / Sessions | Redis |
| Auth | JWT (jsonwebtoken), bcrypt/bcryptjs |
| Validation | Zod 4 |
| Payments | Stripe |
| File storage | Cloudinary, Multer |
| PDF generation | html-pdf-node (certificates) |
| Nodemailer | |
| Scheduling | node-cron |
| Logging | Winston, Morgan |
| Monitoring | swagger-stats |
| Testing | Vitest, Supertest |
| Package manager | pnpm 10 |
Almost every top-level entity (Course, License, Payment, Package, Quiz, Coupon, Notification, Alert, SupportTicket, ArchiveSubscription, Certificate, Review) carries an optional tenantId. Requests from LICENSEE and tenant-scoped users are automatically filtered by tenant via a tenantGuard middleware.
| Level | Description |
|---|---|
PRIVATE_USER |
Individual learner |
COMPANY_ADMIN / COMPANY_EMPLOYEE |
B2B company accounts |
LICENSEE |
Runs a branded catalog under a tenant, earns revenue share |
PLATFORM_ADMIN |
Full cross-tenant access |
TUTOR / TEACHER |
Assigned to specific courses |
NOT_STARTED โ IN_PROGRESS โ COMPLETED (or EXPIRED / SUSPENDED), driven by LessonProgress / ScormSession records. When all required lessons are completed, enrollmentService.checkAndUpdateEnrollmentStatus() auto-marks the enrollment COMPLETED and triggers async certificate generation.
A Payment is the central commerce record โ it can back a single-course Enrollment, a License, a PackagePurchase, a CompanyCoursePurchase, or an ArchiveSubscription, and produces an Invoice. Renewals are tracked separately (CourseRenewal, LicenseRenewal, CompanyCoursePurchaseRenewal) so pricing history isn't lost.
Fields like courseTitle, description, name, message are stored as Prisma Json โ typically { "it": "...", "en": "...", "fr": "...", "zh": "..." }. Frontend clients should resolve these using the current SupportedLocale and the i18nMiddleware.
diego-backend/
โโโ prisma/
โ โโโ migrations/
โ โโโ schema.prisma
โโโ src/
โ โโโ config/ # db, logger, env config
โ โโโ features/
โ โ โโโ assignCourse/
โ โ โโโ auth/
โ โ โโโ certificate/
โ โ โโโ course/
โ โ โโโ coursePurchase/
โ โ โโโ employee/
โ โ โโโ enrollment/ # โ
fully documented โ see API docs
โ โ โโโ lesson/
โ โ โโโ license/
โ โ โโโ licenseIncome/
โ โ โโโ licensePlan/
โ โ โโโ notification/
โ โ โโโ package/
โ โ โโโ payment/
โ โ โโโ quiz/
โ โ โโโ reviews/
โ โ โโโ supportTicket/
โ โ โโโ user/
โ โ โโโ each feature/ # {feature}.controller.js, {feature}.service.js,
โ โ # {feature}.routes.js, {feature}.validation.js
โ โโโ generated/prisma/ # Prisma client output
โ โโโ routes/ # route aggregator
โ โโโ seeds/ # admin.seeder.js, package.seeder.js
โ โโโ shared/
โ โ โโโ globals/
โ โ โโโ decorators/ # catchAsync
โ โ โโโ helpers/ # auth-middleware, tenant.middleware, i18n.middleware, response.handler
โ โโโ app.js
โ โโโ app.test.js
โ โโโ bootstrap.js
โ โโโ routes.js
โ โโโ server.js
โโโ uploads/
โโโ .env.dev
โโโ package.json
Every feature module follows the same 4-file pattern: *.routes.js โ *.controller.js โ *.service.js, validated by *.validation.js (Zod). This means once a frontend dev understands the Enrollment module (fully documented below), every other module behaves the same way structurally.
- Node.js 18+
- PostgreSQL database
- Redis
- pnpm (
npm install -g pnpm)
# 1. Clone the repository
git clone <repo-url>
cd diego-backend
# 2. Install dependencies
pnpm install
# 3. Set up environment variables
cp .env.dev .env
# Edit .env with your credentials
# 4. Generate Prisma Client
pnpm run prisma:generate
# 5. Run database migrations
pnpm run prisma:migrate
# 6. Seed the database
pnpm run seeds:admin # Create admin account
pnpm run seeds:package # Create subscription packages
# 7. Start development server
pnpm run devServer runs at http://localhost:5000/api/v1
Create a .env file (copy from .env.dev) with at least:
# Server
NODE_ENV=development
PORT=5000
API_PREFIX=/api/v1
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/diego_lms
# Redis
REDIS_URL=redis://localhost:6379
# Auth
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=7d
JWT_REFRESH_SECRET=your_refresh_secret
# Stripe
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Cloudinary
CLOUDINARY_CLOUD_NAME=xxx
CLOUDINARY_API_KEY=xxx
CLOUDINARY_API_SECRET=xxx
# Email (Nodemailer)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=xxx
SMTP_PASS=xxx
# Frontend URL (used in emails / access links)
FRONTEND_URL=http://localhost:3000
โ ๏ธ Ask your backend lead to confirm the exact variable names against.env.devโ this list is inferred from the dependency list (Stripe, Cloudinary, Redis, Nodemailer, JWT) and should be verified.
| Script | Description |
|---|---|
pnpm run dev |
Start development server with nodemon auto-reload |
pnpm start |
Start production server (node src/bootstrap.js) |
pnpm run build |
Generate Prisma Client |
pnpm run postinstall |
Auto-runs prisma generate after install |
pnpm run prisma:generate |
Generate Prisma Client |
pnpm run prisma:migrate |
Run database migrations (dev) |
pnpm run prisma:deploy |
Deploy migrations (production) |
pnpm run prisma:studio |
Open Prisma Studio GUI |
pnpm run seeds:admin |
Seed admin user |
pnpm run seeds:package |
Seed subscription packages |
pnpm run lint |
Run ESLint |
pnpm run lint:fix |
Auto-fix ESLint issues |
pnpm run format |
Format code with Prettier |
pnpm run test |
Run tests with Vitest |
pnpm run test:watch |
Run tests in watch mode |
pnpm run test:coverage |
Run tests with coverage report |
- Base URL:
http://localhost:5000/api/v1(dev) - Auth:
Authorization: Bearer <accessToken>header on all protected routes - Content-Type:
application/json - Response envelope (via
ResponseHandler):
{
"success": true,
"message": "Human readable message",
"data": { }
}Error responses follow the same envelope with "success": false and an "error" or "message" field describing what went wrong (validation errors surface Zod's issue array).
- Pagination (list endpoints): query params
page(default 1),limit(default 20, max 100), returns:
{
"meta": { "page": 1, "limit": 20, "total": 42, "totalPages": 3 },
"items": [ ]
}- Multi-tenant scoping:
PLATFORM_ADMINsees everything (optionally filter with?tenantId=);LICENSEEand tenant-bound users are auto-scoped to their owntenantIdโ no need to pass it manually. - i18n fields: any field typed
Jsonin the schema (titles, descriptions, messages) may come back as an object keyed by locale ({ "en": "...", "it": "..." }) rather than a plain string.
| Module | Status | Notes |
|---|---|---|
| enrollment | โ Fully documented | See API_DOCUMENTATION.md + Postman |
| course | ๐ก Partial (sample response only) | List endpoint shape known from sample; CRUD endpoints to confirm |
| auth | โฌ Pending source | Login/register/refresh/OTP endpoints โ send auth.routes.js |
| certificate | โฌ Pending source | certificateService.autoGenerateOnCompletion confirmed to exist |
| coursePurchase | โฌ Pending source | |
| employee | โฌ Pending source | |
| lesson | โฌ Pending source | |
| license | โฌ Pending source | |
| licenseIncome | โฌ Pending source | |
| licensePlan | โฌ Pending source | |
| notification | โฌ Pending source | |
| package | โฌ Pending source | |
| payment | โฌ Pending source | Stripe-backed |
| quiz | โฌ Pending source | |
| reviews | โฌ Pending source | |
| supportTicket | โฌ Pending source | |
| user | โฌ Pending source | |
| assignCourse | โฌ Pending source |
Send the
.controller.js/.routes.js/.validation.jsfor any โฌ module and it'll be added toAPI_DOCUMENTATION.mdand the Postman collection in the exact same format as Enrollment.
All routes in enrollment.routes.js (and, by convention, every other feature) run through two global middlewares:
router.use(authMiddleware.protect); // validates JWT, attaches req.user
router.use(i18nMiddleware); // resolves locale for Json fieldsElevated routes add:
const adminGuard = authMiddleware.authorize('PLATFORM_ADMIN', 'LICENSEE');
router.use(adminGuard, tenantGuard);For the frontend:
- Obtain
accessTokenfrom the (pending)authmodule's login endpoint. - Send it as
Authorization: Bearer <accessToken>on every request. - A
403/401on alicensee/*or admin route usually means the logged-in user'slevelisn'tPLATFORM_ADMINorLICENSEE, or theirtenantIddoesn't resolve.
Import DIEGO_LMS.postman_collection.json into Postman. It includes:
- Collection-level
{{baseUrl}}and{{accessToken}}variables - A Enrollment folder with all 14 endpoints, pre-filled example bodies matching the Zod schemas, and inline descriptions
- A Courses folder with the confirmed
GET /courseslist endpoint (from the sample response you shared) - Placeholder folders for every other module, ready to fill in once source files are shared
Set {{baseUrl}} to http://localhost:5000/api/v1 and {{accessToken}} after logging in.
API_DOCUMENTATION.mdโ full endpoint-by-endpoint reference (request/response/auth/errors)prisma/schema.prismaโ source of truth for all data shapes- Prisma Studio (
pnpm run prisma:studio) โ browse live data during integration testing