Skillory
Learning Management System
Skillory is a full-stack learning marketplace where students discover, purchase, and watch expert-led courses, and admins run the entire catalog, user base, and site content from one dashboard.
Project overview
Skillory is built around two roles working from very different surfaces of the same platform. Students discover courses, pay for them through an embedded Stripe flow, watch DRM-protected video content, and ask questions or leave reviews directly on each lecture. Admins manage the full course catalog through a multi-step builder, control site content like the homepage banner and FAQ, manage user roles, and get real-time visibility into new orders, questions, and reviews as they come in, all backed by Redis-cached sessions and a Socket.io notification layer.
A course player built around real access control
The public course page shows full details, pricing, and a curriculum preview to anyone, but the actual video player is gated behind purchase status, checked on both the frontend and the backend. Video itself streams through VdoCipher's OTP-based secure playback rather than a plain video URL, so access can't be bypassed by simply finding a link.

A multi-step course builder for admins
Creating a course walks an admin through Course Information, Benefits and Prerequisites, Course Content, and a final Preview, each step holding its own piece of shared state. Lectures get grouped into collapsible sections while authoring, then flattened into the database's schema on submit, and re-grouped by section again when the course is edited or displayed publicly.

Real-time admin dashboard with live analytics
New orders, questions, and reviews broadcast to the admin dashboard instantly through Socket.io, with a notification sound and unread badge. Analytics charts for courses, users, and orders are computed from rolling 28-day buckets and rendered with Recharts, alongside MUI DataGrid tables for browsing every course, user, and invoice.

Key features
Multi-role platform
Students browse, purchase, and watch courses. Admins get full CRUD over courses, users, and site content from a dedicated dashboard.
Email and social authentication
Email and password registration with activation codes, plus Google and GitHub sign-in through NextAuth, bridged into the app's own JWT session.
Secure embedded payments
An embedded Stripe Payment Element with server-computed pricing, backed by a webhook as an independent fulfillment safety net.
Per-lecture Q&A and reviews
Students ask questions on individual lectures and leave course reviews, with admins able to reply publicly to either.
Real-time notifications
Socket.io broadcasts new orders, questions, and reviews live to the admin dashboard, backed by persisted notification history.
Full dark and light theming
Class-based dark mode applied consistently across custom components and third-party libraries like MUI and Stripe Elements alike.
2
User roles
6
API route groups
7
Database collections
1
Real-time channel
Architecture & diagrams
The Next.js frontend talks to an Express backend across seven modules, each reaching MongoDB, Redis, Stripe, Cloudinary, or VdoCipher as needed. The frontend deploys to Vercel, the backend to Render as a persistent service.
Challenges & solutions
| Challenge | Solution |
|---|---|
| Server and client rendering disagreed on first paint for theme icons and session-dependent UI, causing hydration mismatches. | Added a mounted state flag set inside a useEffect, rendering a neutral placeholder until after hydration completes so server and client output always agree on first paint. |
| Tailwind v4's dark: variant defaulted to the OS-level color scheme instead of the app's own theme toggle. | Rebound the dark: variant to a class-based check matching how next-themes actually toggles the theme, rather than relying on the media-query default. |
| Field name casing mismatches between frontend and backend, like videoUrl versus videoURL, caused silent data loss with no visible error. | Traced each mismatch by comparing the actual API response shape against what the frontend read, then aligned field names exactly on both sides. |
| Stripe webhook signature verification was failing silently. | Registered the webhook route with Express's raw body parser before the app's global JSON middleware, since signature verification needs the untouched raw request body. |
| Course reviews and replies weren't appearing on the public course page after being added, despite saving correctly to the database. | Found a 7-day Redis cache on the course document that was never refreshed after review or reply writes, and added a cache-set call immediately after each successful save. |
| MUI DataGrid's internal styling stopped responding to standard style overrides for dark mode after a version update moved theming into internal CSS variables. | Layered important overrides across every relevant DataGrid sub-selector, headers, cells, rows, pagination, and icons, to guarantee the override regardless of the installed version's internals. |
| Next.js 15 made route params an async value instead of a plain object, breaking existing param access. | Unwrapped params with React's use() hook in Client Components, and with a plain await in Server Components. |
Best practices
Authentication & security
- Access and refresh tokens stored as HTTP-only cookies, never exposed to client-side JavaScript
- Payment amounts always recalculated server-side from the course document, never trusted from client input
- Every admin-only route protected by both authentication and role-authorization middleware
Component architecture
- Multi-step course form lifts all state to one parent component to avoid prop drilling
- A single generic confirmation dialog and modal wrapper reused across delete, login, and checkout flows
- One consistent color token system applied identically across custom components and third-party libraries
Error handling & UX
- Toast-based feedback for every mutation across the app
- Loading, empty, and populated states distinguished everywhere data is fetched
- UI updates like marking a notification read only commit once the request has genuinely succeeded