Building ACA Athlons: An Honest Walkthrough of Our LMS Sprint
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 scho

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
-
bcryptwith 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":
-
Exam engine — timer, auto-save, anti-cheat, question shuffling, review step
-
Assignment discussion threads — entity, API, and UI
-
Rich text editors — for assignment descriptions, curriculum content, announcements
-
Integration tests — cover cohort isolation, grade calculation, token rotation
-
Peer evaluation for group projects
-
Curriculum-embedded video player — fix the recording/lesson disconnect
-
Drag-and-drop curriculum reordering
-
Cohort leaderboard z-score normalisation
-
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