codexmachina
App schema

LMS (learning platform), built on every verified stack.

Learning platform: course catalog (courses → modules → lessons), enrollment join, and per-lesson learner progress tracking.

40 verified stacks40 greenLast updated 2026-08-23How we verify →

40 verified stacks

Showing 12 across 1 app types. Every stack page links its neighbours along each axis, so any combination is a click or two from here.

Next.js 16 (App Router) + MySQL 8Auth.js (NextAuth) · Resend
Next.js 16 (App Router) + MySQL 8Better Auth
Next.js 16 (App Router) + MySQL 8Clerk · Resend
Next.js 16 (App Router) + MySQL 8Supabase Auth
Next.js 16 (App Router) + Postgres (Neon)Auth.js (NextAuth)
Next.js 16 (App Router) + Postgres (Neon)Better Auth · Resend
Next.js 16 (App Router) + Postgres (Neon)Clerk
Nuxt 4 + MySQL 8Supabase Auth · Resend
Nuxt 4 + Postgres (Neon)Better Auth · Resend
Nuxt 4 + Postgres (Neon)Clerk
Nuxt 4 + Postgres (Neon)Supabase Auth
React Router v8 + MySQL 8Better Auth · Resend

What LMS (learning platform) gives you

Five tables sit on top of the identity the auth fragment declares: courses, modules, lessons, enrollments and lesson_progress. The catalog is a strict three-level tree — a course owns ordered modules, a module owns ordered lessons — and both ordering indexes are composite with the parent id first and position second (idx_module_course, idx_lesson_module), so rendering a syllabus is two range scans that come back already sorted. Learner state hangs off the side of that tree rather than inside it. enrollments is a course↔student join carrying enrollments_course_student_unique, which turns a double enrolment into a constraint violation instead of a duplicate row, while idx_enrollment_student answers the same question from the learner's side. lesson_progress is one row per (student, lesson), enforced by lesson_progress_student_lesson_unique and read through idx_progress_student_lesson.

The shape to understand before adopting it: progress is not tied to enrolment. lesson_progress references lessons.id and user.id directly, with no path back to an enrollments row, so a progress record can exist for someone who never enrolled — deciding whether it counts means joining lessons to modules to courses to enrollments in application code. Course-level completion is derived too. There is no counter column anywhere, so twelve-of-twenty-lessons-done is a COUNT over lesson_progress filtered by status; it stays correct under concurrent writes and costs a join per course card. Statuses are text with CHECK constraints — courses_status_check, lessons_content_type_check, lesson_progress_status_check — rather than pg enums, so shipping a scheduled course status or an assignment content type is a constraint swap, not an ALTER TYPE.

Every foreign key cascades, and that cascade reaches further than it first looks: courses.instructor_id references user with ON DELETE CASCADE, so deleting an instructor account removes their courses and, through modules and lessons, every student's progress underneath them. If instructors churn, deactivate rather than delete.

Built for

The full curriculum for one course, in teaching order

modules are read through idx_module_course (course_id, position) and lessons through idx_lesson_module (module_id, position); both indexes lead with the parent id and end in position, so the outline arrives sorted and never needs a separate sort step.

Every course a given learner is enrolled in

idx_enrollment_student on enrollments.student_id collects the learner's rows, each joining to courses by primary key — the my-courses shelf is one index scan plus PK lookups.

The roster for a course, with no duplicate learners

enrollments_course_student_unique on (course_id, student_id) serves the course-side scan through its leading column and guarantees one row per pair; a repeat enrolment fails at the constraint instead of quietly doubling the roster.

Whether this learner has finished this lesson

lesson_progress carries idx_progress_student_lesson plus the unique on (student_id, lesson_id), so a lesson page resolves at most one progress row, and lesson_progress_status_check keeps status to not_started, in_progress or completed.

An instructor's own catalog, and the course page behind a slug

idx_course_instructor on courses.instructor_id lists everything one teacher owns; courses.slug is unique and backed by idx_course_slug, so a /courses/<slug> route resolves to exactly one row.

Why LMS (learning platform), specifically

note

Enrollments carry a composite unique on (courseId, studentId) — re-enrolling the same student in the same course is a constraint violation, not a duplicate row.

note

lessonProgress uses text + CHECK over pgEnum for status, keeping 'not_started'/'in_progress'/'completed' extensible without an ALTER TYPE migration.