We built Athlons in a sprint. A real one — not the kind where you say "sprint" and mean three comfortable weeks. Athlons is the Learning Management System for Attueyi Coding Academy (ACA), a tech school based at the University of Nigeria, Enugu Campus. The brief was clear: build a production-grade LMS from a detailed Software Requirements Document. Multi-role. Cohort-isolated. Gamified. Branded.

This post is my honest walkthrough of what we built, what actually works well, and where we left IOUs in the codebase.


The Architecture: Good Foundations

Before diving into features, the architecture deserves acknowledgment because it was the right call.

We went with a pnpm + Turborepo monorepo with three workspaces:

  • apps/api — NestJS 11 backend

  • apps/web — React 19 + Vite + TanStack Router frontend

  • packages/shared — Zod schemas, TypeScript types, constants shared between both apps

That packages/shared workspace is the silent hero. Every DTO on the backend has a Zod counterpart that the frontend also validates against. API contract drift — that class of bugs where your frontend sends { userId } and your backend expects { user_id } — is architecturally impossible here. A breaking change in a shared schema fails both the API build and the frontend build at the same time.

The tech stack:

  • Backend: NestJS, TypeORM, PostgreSQL, Redis, BullMQ, JWT, class-validator

  • Frontend: React 19, TanStack Router (file-based routing), TanStack Query, Zustand, Tailwind CSS

  • Infrastructure: PM2 + Nginx on VPS, GitHub Actions CI, Docker Compose for local dev


The Good: What Actually Works

Authentication & Security

The auth system is solid. We implemented:

  • Email + password login for all three roles (Student, Instructor, Admin)

  • Short-lived JWT access tokens (15-min) with long-lived rotating refresh tokens (7-day)

  • Refresh token rotation — every token is single-use. If an attacker steals a refresh token and tries to use it after the legitimate user already rotated it, the server detects the replay and invalidates the entire session family

  • Account lockout after 5 consecutive failed login attempts with a 30-minute cooldown

  • Optional 2FA via TOTP (authenticator app) and email OTP

  • Backup codes for account recovery

  • bcrypt with cost factor 12 for password hashing

The frontend's Zustand auth store persists to localStorage and automatically injects the JWT into every Axios request via an interceptor. On a 401, the interceptor silently attempts a token refresh before retrying the original request. The user never sees a login prompt mid-session unless the refresh token itself has expired.

Cohort Isolation — The Heart of the System

Every student at ACA belongs to one cohort, and one cohort only. "Cohort 5 students should never see Cohort 4 data" is not just a UI rule — it's enforced at the middleware layer on every single authenticated API request.

The CohortScopeMiddleware reads the X-Cohort-Id header sent by the frontend, validates that the requesting user actually belongs to that cohort, and attaches the validated activeCohortId to the request object. Individual service methods never have to think about isolation — they receive an already-validated cohort scope and query within it.

Admins are the only exception: they can switch cohort context via a dropdown and view any cohort's data, which feeds the analytics and audit surfaces.

The Gamification System

This was the fun one. The student dashboard is not a table of grades — it's a player profile.

The dashboard renders a full gamification layer over academic data:

  • PlayerHeroCard — student avatar, level, title, and XP bar

  • XPProgressCard — progress to the next level with animation

  • StreakFlameCard — current daily login streak with flame intensity

  • DailyQuestList — actionable micro-challenges (submit an assignment, check the leaderboard, mark 3 lessons complete)

  • WeeklyMissionCard — a broader weekly goal

  • AchievementShelf — earned badges displayed like trophies

  • SocialRankCard — rank within the cohort/track

  • ActivityTimeline — recent academic events rendered as a game activity feed

  • CelebrationLayer — confetti and overlay animations when a quest is claimed or level achieved

On the backend, this is backed by a full gamification engine: XP service, level service, streak service, achievement service, quest service, reputation service, and a leaderboard cache service. All of it runs asynchronously through BullMQ queues — an XP award fires an event into the queue, and the processor handles updating the database and invalidating the Redis leaderboard cache without blocking the original API request.

There's even an AdaptiveQuickActions system that analyzes the student's current state (upcoming deadlines, pending quests, unread announcements) and surfaces the single most important action they should take right now.

Real-Time Messaging

We shipped a full messaging system backed by Socket.IO. Students, instructors, and admins can communicate in group channels (scoped to their cohort) or via direct messages. The MessagesWorkspace component mirrors a Slack-style sidebar with channels on the left and a conversation view on the right.

On the backend, the MessagesGateway (NestJS WebSocket gateway) handles connection management, join/leave events, and real-time message delivery. A messages-cache.service.ts uses Redis to cache recent message history for fast initial page loads.

Assignment, Attendance, and Gradebook Surfaces

All three roles have complete, functional CRUD surfaces for their respective workflows:

  • Instructors can create assignments with due dates, grace periods, late penalties, and maximum file size constraints

  • Students can submit files or text and track submission status (on time, late, graded)

  • Attendance sessions are creatable per class, with Present/Absent/Late/Excused statuses and a bulk-mark feature

  • Gradebook shows a per-student breakdown of assignment grades, exam scores, attendance percentage, and weighted total

  • Leaderboard surfaces exist for both track-level and cohort-level rankings, cached in Redis

Background Jobs & Exports

Heavy operations run via BullMQ without blocking the API:

  • Export generation (CSV/XLSX/PDF) runs in a background processor and sends a notification when the file is ready

  • Certificate generation runs as a batch job

  • Leaderboard recalculation happens asynchronously after any grade change

DevOps & CI

The CI pipeline (GitHub Actions) runs lint, typecheck, unit tests (with live Postgres + Redis service containers), and build on every push. The deployment story is thorough: PM2 ecosystem config, Nginx config templates, a VPS bootstrap script, and a pre-deployment checklist in /docs/deployment/.


The Bad: Gaps Between the Spec and Reality

Now the honest part.

The Exam Engine Is Underpowered

The SRD specified an exam engine that would make a test-prep platform proud:

  • Countdown timer displayed prominently during the exam

  • Auto-save every 30 seconds (server-side)

  • Anti-cheat: detect tab switches (visibilitychange), fullscreen exits, copy-paste attempts

  • Auto-submit on 3 tab switches or 2 fullscreen exits

  • Question order randomised per student

  • MCQ option order randomised per question per student

  • A review page before final submission showing answered, flagged, and unanswered questions

What shipped: a basic exam page that lists questions, accepts text answers, and has a submit button. There is no timer UI, no auto-save, no anti-cheat detection, no question shuffling, no review step. The entity model supports durationMinutes and availableFrom/availableUntil — the scaffolding is there — but the exam taking experience is the component-level work that didn't happen.

This is the biggest functional gap. For a school running formal examinations, this matters.

Assignment Discussion Threads

FR-05.10 specified that every assignment should have a discussion thread — students ask clarifying questions, the instructor pins key answers. This is absent. There's no comments or discussion_entries entity in the database and no discussion UI in the assignment detail views. The social learning layer around assignments is simply not there yet.

Peer Evaluation for Group Projects

FR-13.6 described a peer evaluation form where team members rate each other's contributions on a 1–5 scale with comments. The project CRUD exists, team creation is modeled, but peer evaluation is not implemented at either the API or UI layer.

Drag-and-Drop Curriculum Reordering

FR-09.4 required that instructors be able to reorder curriculum modules and lessons via drag-and-drop. The displayOrder field exists on both Module and Lesson entities. The instructor can create modules and lessons and the order is stored — but the drag-and-drop reordering UI is not there. You'd need to delete and recreate to change the order.

Cohort Leaderboard Z-Score Normalisation

The SRD specified that the cohort-level leaderboard (which compares students across different tracks) must use z-score normalisation to account for the fact that a Web Development track and a Data Science track might have very different grade distributions. Without normalisation, a track with inflated grades would dominate the cohort ranking. The backend has the leaderboard infrastructure and Redis caching, but the z-score calculation is not implemented.

Certificate QR Verification Endpoint

FR-22.4 specified that each certificate should contain a QR code linking to a public verification endpoint on the ACA main site. Students can download their certificates, but the QR code links to a non-existent verification endpoint. The certificate generation runs as a background job and the PDF is produced, but verification is not wired up.

No Rich Text Editors

Multiple features in the SRD (assignment descriptions, curriculum lesson content, announcement bodies, project briefs) called for rich text editors. The current implementation uses plain <Textarea> for all text input. There's no Tiptap, Quill, or similar editor integrated. This makes the curriculum creation experience notably plain for instructors.

Recordings Are Decoupled from Curriculum

The recordings page is implemented as a standalone route rather than being embedded within the curriculum lesson view. FR-09.3 specified that lessons have an optional videoUrl, and FR-10.4 said videos should be embedded directly in the lesson detail view. What exists is a separate "Recordings" management page for instructors and a standalone recordings listing for students — not the contextual video player embedded within a lesson.


The Ugly: Things That Need Attention

No Tests Against Core Business Logic

The test files exist (auth.service.spec.ts, cohorts.service.spec.ts, gamification.processor.spec.ts) but comprehensive coverage of the critical paths from the TDD — cohort isolation, grade calculation weighted averages, exam auto-submission, webhook HMAC verification, assignment grace period logic — is sparse. The CI runs tests, but the confidence ceiling is low.

Frontend Feature Flag Is a Single Switch

The frontend has exactly one feature flag:

export const featureFlags = {
  gamificationDashboardEnabled: import.meta.env.VITE_FEATURE_GAMIFICATION_DASHBOARD !== 'false',
};

For a platform with this many surfaces and partial implementations, a richer flag system (per-feature, role-aware) would allow progressive rollout and let you safely deploy while hiding unfinished surfaces.

The Exam Entity Has No Anti-Cheat Fields

The Exam entity doesn't have columns to store tabSwitchCount or violationType — these would need to live on the ExamAttempt entity. The entity scaffolding for anti-cheat isn't there, which means adding it requires a migration and database schema changes, not just frontend JavaScript.

Instructor Recording vs Curriculum Split

The recordings feature was built as a standalone module rather than being integrated into the curriculum. This means if an instructor wants to associate a recording with a lesson, they can't — the two data models are disconnected. This will require a data migration and a UI refactor to fix properly.


What's Next

In priority order, here's what would move Athlons from "solid foundation" to "production-ready for an active cohort":

  1. Exam engine — timer, auto-save, anti-cheat, question shuffling, review step

  2. Assignment discussion threads — entity, API, and UI

  3. Rich text editors — for assignment descriptions, curriculum content, announcements

  4. Integration tests — cover cohort isolation, grade calculation, token rotation

  5. Peer evaluation for group projects

  6. Curriculum-embedded video player — fix the recording/lesson disconnect

  7. Drag-and-drop curriculum reordering

  8. Cohort leaderboard z-score normalisation

  9. Certificate QR verification endpoint (requires main site coordination)


Closing Thoughts

What we built is legitimately impressive for the timeframe. A multi-role, cohort-isolated, gamified LMS with real-time messaging, background job processing, Cloudinary uploads, 2FA, audit logging, and a full CI/CD pipeline — that's not nothing.

The gamification system is the thing I'm most proud of. The idea that a student's first view of their school dashboard is a player profile — with XP bars and streak flames and an achievement shelf — is exactly the kind of product thinking that can change the experience of learning at ACA.

The foundation is right. The architecture is right. The data model covers the full scope of the SRD. The gaps are all at the interaction layer — the exam experience, the discussion threads, the rich content editing. Those are hard UI problems, not hard infrastructure problems.

Athlons is ready to run a cohort. Whether it can serve one well depends on which of these gaps gets closed first.


Athlons is the LMS for Attueyi Coding Academy Ltd, University of Nigeria, Enugu Campus. Production domain: athlons.attueyicoding.academy