The LMS Academy home page, with the headline Learn with structured courses and real instructors and a course progress card

The problem

Training academies need one place to sell online courses and downloadable resources, teach them with video lessons, quizzes, projects, questions and live sessions, and follow their students. Students need one place to buy, learn and get feedback.

The approach

An academy builds courses from modules and ordered video lessons, adds quizzes and a final project, and sells the courses and digital products in Tunisian dinars, with discount coupons. Students follow the lessons in a course player, take module quizzes, ask questions under each lesson, send their project for review and book live sessions with the academy. A platform admin creates the academies and chooses their admins.

The browser loads the Next.js web app, whose pages call the NestJS REST API with a sign-in token. The API runs every request through the same checks, then a service applies the rule and talks to PostgreSQL through Prisma.

Who uses it

Students
Buy courses and products, follow the lessons, take quizzes, send projects, ask questions and book live sessions.
Academy admins
Build and sell their academy's courses, confirm payments, review projects, answer questions and run sessions.
Platform admins
Create the academies, choose their admins and help with password resets.

What it does

  • A public course catalog with search, and a page per course with its modules and lessons
  • An academies directory, and a page per academy with its courses
  • A store of digital products, such as guides, templates and source code, sold as download links
  • Checkout with discount coupons, with payments confirmed by hand by the academy
  • A course player with video lessons, lessons marked as done and progress per course
  • Module quizzes with multiple-choice and true/false questions, attempts and a best score
  • Questions under each lesson, with replies and an accepted answer
  • Course projects that students send with their repository and live links, and that the academy approves or sends back
  • Live sessions: students request mentoring or webinars, and the academy confirms them and adds the meeting link
  • An academy admin space with a dashboard, a course editor, students with a CSV download, orders, coupons and products
  • A platform admin space for academies, their admins and password reset links
  • API documentation with Swagger while developing

Where the engineering is

Rights that change at once

A sign-in token issued before a role change or a password change must not keep its old rights. The API reads the account and its role from the database on every request and compares the version number carried by the token, so changing the password, or choosing to sign out everywhere else, makes the older tokens stop working.

Passwords are hashed with argon2, and the three roles are checked by guards on every request.

Money in dinars, counted once

Prices are decimal amounts in Tunisian dinars. Every computed amount, such as a coupon discount or an order total, is rounded to the millime, and a price accepts at most three decimals.

Payments are confirmed by hand by the academy. Marking an order as paid is one conditional update inside a transaction, so two admins cannot confirm it twice and a coupon use is counted only once.

The digital products store, with a search box and three products from two academies, priced in TND, Open the full-size image (opens in a new tab)
Digital products, priced in Tunisian dinars.

One platform, many academies

Several academies share one platform, so every admin query goes through the academy of the admin who makes it. An academy never sees or changes another academy's courses, students or orders.

The data lives in 17 related tables, from courses, modules and lessons to orders, coupons and bookings, with foreign keys between them and 14 migrations that are only ever added, never reset.

The database schema, with tables such as User, Center, Stage, Module, Lesson, Order, Coupon, Booking and their relations, Open the full-size image (opens in a new tab)
The database schema, drawn from the Prisma schema.

Every request through the same checks

The API checks each request in a fixed order: security headers, the list of allowed sites, a rate limit of 300 requests a minute and 10 for sign-in and sign-up, the token, the role, and then the body. Bodies are validated by 32 DTO classes: unknown fields are refused and HTML is removed.

The environment variables are validated when the API starts, and the Swagger documentation of the 85 endpoints is available while developing but off in production unless asked for.

The Swagger page of the LMS Academy API, listing user and authentication endpoints, Open the full-size image (opens in a new tab)
The API documentation, while developing.

A local database that is safe to set up

The setup script first checks that the database belongs to this app, and refuses one that holds another project's tables. Then it applies only the migrations that are missing.

Screens

How it's built

LayerTechnology
Web appNext.js 16 and React 19: 44 page files and 21 components, with one dark theme
StylingTailwind CSS 4, with a visible keyboard focus and text colors that meet WCAG AA contrast
APINestJS 11 with 85 endpoints in 16 controllers, and guards and pipes for sign-in, roles and validation
DatabasePostgreSQL with Prisma 7: 17 tables, 7 enums and 14 migrations
SecurityJWT with a version number, argon2 password hashing, Helmet headers, rate limits and HTML cleaning
TestsJest for the API and the Node.js test runner for the web app, with Prisma replaced by stubs
Architecture diagram: the browser, the Next.js web app, the NestJS REST API with its checks in order, and PostgreSQL, Open the full-size image (opens in a new tab)
From the browser to the database, with the checks the API runs in order.

Testing

498 unit tests pass: 352 for the API in 76 files, 136 for the web app and 10 for the scripts. They run without a database, with Prisma replaced by stubs.

A route check also follows every internal link of the web app: 44 pages and 114 links.

Results

  • 44 page files in the web app and 21 components.
  • 85 API endpoints in 16 controllers.
  • 17 database tables and 7 enums, changed by 14 migrations.
  • 498 unit tests, all passing on the last run.

What I would build next

  • Online payments, instead of an academy confirming each payment by hand.
  • Emails for password resets, instead of a link made by the platform admin.
  • End-to-end tests for buying, learning and reviewing.
  • Pagination for the lists that are loaded whole.

Built with

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • NestJS
  • PostgreSQL
  • Prisma
  • Jest

56 results