Learning Management System
Part of the Full Stack Developer Roadmap
In short
You're using one right now. Courses, lessons, progress ticks and quizzes look simple, but tracking progress per user, per lesson, without slowing the page, is a real design problem.
A course platform with lessons, progress tracking and quizzes — directly relevant given the roadmap tool you're using right now follows a similar UX pattern.

Think of it like this
The tuition teacher's register
Sir keeps one register: a row per student, a column per chapter, a tick when you finish. A glance shows you're 60% done. Your LMS is that register, with the database keeping the ticks.
Watch it happen
Marking a lesson done
POST /lessons/31/done
1/5The student clicks 'Mark as done' at the end of a lesson.
The big ideas
What you need to know
Course, module, lesson
A clear hierarchy with an order column at each level, so you can reorder lessons without renaming them.
Progress is a join table
lesson_progress(user_id, lesson_id) with both as the primary key. Done is simply 'row exists'.
Quizzes checked on the server
Send questions without answers. The server grades the attempt and stores the score.
Video from storage
Store videos in object storage or a video service, and serve signed URLs to enrolled students only.
See it in code
Walk through the code
1CREATE TABLE lesson_progress (2 user_id bigint REFERENCES users(id),3 lesson_id bigint REFERENCES lessons(id),4 completed_at timestamptz NOT NULL DEFAULT now(),5 PRIMARY KEY (user_id, lesson_id)6);7 8-- Mark done (safe to call twice)9INSERT INTO lesson_progress (user_id, lesson_id) VALUES ($1, $2)10ON CONFLICT DO NOTHING;11 12-- Course progress for the course page13SELECT round(100.0 * count(lp.lesson_id) / nullif(count(l.id), 0)) AS percent14FROM lessons l15LEFT JOIN lesson_progress lp ON lp.lesson_id = l.id AND lp.user_id = $116WHERE l.course_id = $2;1/3The pair is the primary key, so each student can complete each lesson only once.
Progress is just rows in a join table. One idempotent insert marks a lesson done, and one LEFT JOIN works out the percentage.
Save yourself the pain
Common mistakes
Oops
Sending quiz answers to the browser and grading in React.
Do this instead
Keep answers on the server. The browser sends choices, the server returns the score.
Oops
Storing progress as a JSON blob on the user row.
Do this instead
Use a lesson_progress table. It's easy to query, index and report on.
Your mission
First milestone: one course, real progress
Quick check
A student clicks 'Mark as done' twice on the same lesson. With the table above, what happens?
Go deeper
- 1Model courses as nested data: course → modules → lessons.
- 2Track and persist per-user progress through course content.
- 3Add a quiz feature with scoring.
More projects at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.