Dual-track (Secondary · University) CBT & learning platform — question banks, past papers, timed exams, materials/Telegram, and package-based premium.
Live stack: Next.js 16.3 (Turbopack) + React 19 + Supabase + Paystack + Gemini · Deployed from prepmaster/.
- Overview
- Features
- Tech Stack
- Architecture
- Getting Started
- Environment Variables
- Database & Migrations
- Project Structure
- Scripts
- Theming
- Payments & Premium
- AI Question Generation
- Roles & Access
- Deployment
- Contributing
PrepMaster serves two audiences from one codebase:
- Secondary School — SS1–SS3, subjects (Physics/Chemistry/Maths/Further Maths), WAEC/JAMB/NECO past papers.
- University — faculties/departments/levels/semesters, courses, faculty-scoped content.
Students register by track, pick level/faculty/department/semester, and get a scoped dashboard. Admins manage all reference data, content, exams, and monetization.
- Track-aware registration (Secondary vs University) with level/faculty/department/semester selection.
- Dashboards scoped by track: subjects or courses, materials, Telegram groups.
- Practice & CBTs — free, paid, and premium-gated exams.
- Timed CBT player — debounced autosave, Realtime answer sync (
attemptsrow via Supabase Realtime), localStorage fallback, flagging, countdown, submit & result pages with explanations/re-attempts. - Materials & past papers browsing.
- Packages page (
/dashboard/packages) — buy subscription via Paystack; success/failure notices via?pay=param. - Profile & theme toggle (light/dark).
- Content: Subjects, Courses (code/name/track/level/faculty/department/semester).
- Exams: CBTs & Exams list, question-banks, past papers. Creation wizard (
/admin/exams/new) supports 4 sources: question-bank picker, AI from notes, AI from past papers, manual paste. - Structure: Levels (SS1–SS3, 100L–500L), Faculties & Departments, Users (role management).
- Billing: Packages — list/add/edit/toggle active/delete with pricing (
/admin/packages). - Exam settings: duration, review/re-attempts/explanations toggles, free/premium/price, scheduling, status (draft/scheduled/live/paused/ended).
- Framework: Next.js 16.3.4 (App Router, Turbopack), React 19, TypeScript 5
- Styling: Tailwind CSS v4, Radix UI,
next-themes,lucide-react - Backend: Supabase (Auth, Postgres, RLS, Realtime),
@supabase/ssr - Payments: Paystack (
paystack-jsREST — initialize/verify) - AI: Google Gemini (
GEMINI_API_KEY) for question generation - Tooling: ESLint (
eslint-config-next), Supabase CLI
[ Next.js App Router ]
├─ / (marketing) ─ /login /register /register/complete
├─ /dashboard/* (student, track-aware shell)
├─ /admin/* (admin shell, isAdmin() guard → redirect /dashboard)
├─ /api/paystack/verify (callback → payments.status)
└─ Supabase: Auth · Postgres (RLS) · Realtime (attempts)
Key guards:
src/lib/session.ts:isAdmin()— checksprofiles.role = 'admin'.src/lib/access.ts:hasPremiumSubscription()/hasExamAccess()— premium exams unlock via directpayments.exam_idsuccess or any successfulpayments.package_id.- RLS: reference tables have
authenticated+anonSELECT policies for pre-login registration; all writes are admin-gated (public.is_admin()).
- Node 20+, npm
- Supabase project + CLI (
npx supabase --version≥ 2.116) - Paystack account (test keys okay for local), Gemini API key (optional, for AI features)
git clone https://github.com/Mrweb-devpro/PrepMasterLMS.git
cd PrepMasterLMS
npm installcp .env.example .env.local
# then fill in values (see Environment Variables)# login once (or set SUPABASE_ACCESS_TOKEN)
npx supabase login
npx supabase link --project-ref <your-project-ref> # e.g. ftqbgdavqzxzyirzwrrm
npx supabase db pushThis applies supabase/migrations/00001_initial_schema.sql → 00005_fix_profile_trigger.sql and seeds reference data (tracks, faculties, semesters, subjects). Levels & departments are seeded via app/migration or admin — see Database.
If you run a local Supabase stack instead:
npx supabase init
npx supabase startnpm run dev # http://localhost:3000
npm run build # production build check (27 routes)
npx tsc --noEmit
npx eslint .-
Register at
/register(pick track/level/etc). -
In Supabase Dashboard → Table Editor →
profiles→ set your rowrole = 'admin':update profiles set role = 'admin' where id = '<your-auth-user-id>';
Or via service_role REST:
curl -X PATCH "https://<ref>.supabase.co/rest/v1/profiles?id=eq.<id>" \ -H "apikey: <service_role>" -H "Authorization: Bearer <service_role>" \ -H "Content-Type: application/json" -d '{"role":"admin"}'
-
Visit
/admin.
| Variable | Required | Where | Notes |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
yes | Supabase → Settings → API | https://<ref>.supabase.co |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
yes | Supabase → API | anon/public |
SUPABASE_SERVICE_ROLE_KEY |
yes | Supabase → API | service_role (server-only) |
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY |
for payments | Paystack → Settings → API | pk_test_... |
PAYSTACK_SECRET_KEY |
for payments | Paystack → API | sk_test_... |
GEMINI_API_KEY |
for AI | aistudio.google.com/apikey | question generation |
NEXT_PUBLIC_APP_URL |
yes | — | http://localhost:3000 locally |
Never commit .env.local. SUPABASE_SERVICE_ROLE_KEY and PAYSTACK_SECRET_KEY are server-only.
Migrations in supabase/migrations/ (applied via npx supabase db push):
| File | Purpose |
|---|---|
00001_initial_schema.sql |
Tracks, faculties, departments, levels, semesters, subjects, courses, profiles, questions, banks, exams, attempts, materials, past_papers, packages, payments, telegram_groups, RLS enable, handle_new_user() trigger |
00002_rls_policies.sql |
is_admin() helper, all RLS policies, seed: tracks (Secondary/University), faculties (Engineering/Science/Architecture), semesters, subjects |
00003_exam_payments.sql |
payments.exam_id + index (per-exam Paystack purchases) |
00004_anon_reference_reads.sql |
anon SELECT policies for tracks/faculties/departments/levels/semesters (registration is pre-login) |
00005_fix_profile_trigger.sql |
Fix handle_new_user() to persist track_id/faculty_id/department_id/level_id/semester_id from raw_user_meta_data |
Seeded reference data after db push:
- Tracks: Secondary School, University
- Faculties: Engineering, Science, Architecture
- Departments: 9 seeded (Mechanical/Civil/Electrical/Computer Eng; Computer Sci/Micro/Biochem/Physics; Architecture)
- Levels: 8 (SS1–SS3, 100L–500L)
- Semesters: First/Second
- Subjects: Physics, Chemistry, Mathematics, Further Mathematics
- Packages: create via
/admin/packages(2 demo inserted if empty: Premium Monthly ₦5k, Yearly ₦45k)
prepmaster/
├─ src/
│ ├─ app/
│ │ ├─ admin/ # /admin, /admin/{subjects,courses,exams,levels,faculties,users,packages}
│ │ ├─ dashboard/ # /dashboard, /dashboard/{subjects,courses,practice,cbt,packages,profile}
│ │ ├─ actions/ # payments.ts (initializePackagePayment, initializeExamPayment)
│ │ ├─ api/paystack/verify/
│ │ └─ auth/callback/
│ ├─ components/
│ │ ├─ admin/shell.tsx
│ │ ├─ dashboard/shell.tsx
│ │ └─ ui/ # shadcn-style primitives
│ └─ lib/
│ ├─ supabase/ # server/client
│ ├─ access.ts # hasPremiumSubscription, hasExamAccess
│ ├─ paystack.ts # initializeTransaction, verifyTransaction
│ └─ session.ts # getUser, isAdmin
├─ supabase/migrations/
├─ public/
├─ next.config.ts
└─ package.json
| Script | Description |
|---|---|
npm run dev |
Start dev server (Turbopack) — http://localhost:3000 |
npm run build |
Production build — expect 27 routes |
npm start |
Serve production build |
npx tsc --noEmit |
Type check |
npx eslint . |
Lint (current baseline: ~12 pre-existing issues, no new) |
CSS variables in src/app/globals.css — light (--background: #eff6ff, --card: #ffffff) and dark (--background: #0f172a, --card: #1e293b). Toggle via next-themes (src/components/theme-toggle.tsx). Native <select> elements use bg-background for proper contrast on bg-card.
- Per-exam:
initializeExamPayment(examId)createspaymentsrow (exam_id,status=pending,paystack_reference=pm-…), redirects to Paystackauthorization_url; callback/api/paystack/verify?reference=&exam=verifies viaPAYSTACK_SECRET_KEYand flips tosuccess. - Packages:
initializePackagePayment(packageId)atsrc/app/actions/payments.ts:15— same flow withpkg-prefix and?package=callback. - Gating:
src/lib/access.ts:39— premium exams unlock ifpaymentshasexam_idsuccess oris_premium && hasPremiumSubscription(any successfulpackage_id).
src/app/admin/ai.ts — generateQuestionsFromText calls Gemini REST (GEMINI_API_KEY). Used in exam wizard (create-exam-form.tsx) for AI from notes and AI from past papers sources; stores options as JSONB [A,B,C,D] and correct_answer as letter (A–E).
profiles.roleenum:student | admin | instructor(defaultstudent).isAdmin()(src/lib/session.ts:24) gates/adminlayout (src/app/admin/layout.tsx) → non-admin redirects to/dashboard.- After first registration, promote via SQL/REST as shown in Getting Started.
Any Next.js host (Vercel recommended):
- Set env vars in host dashboard (same as
.env.example). - Point Supabase
NEXT_PUBLIC_APP_URLto deployed URL. - Add Paystack callback URL domain to Paystack dashboard allowlist.
- Push migrations to production Supabase project (
npx supabase link --project-ref <prod-ref> && npx supabase db push).
- Branch from
main:git checkout -b feat/your-feature - Commit with conventional messages (
feat:,fix:,docs:) - Ensure
npx tsc --noEmit && npm run build && npx eslint .is clean (no new issues beyond baseline) - Open a Pull Request against
main— include description, screenshots for UI changes, and migration notes if DB changed
MIT — see LICENSE if present.
Maintained by @Mrweb-devpro · Issues & PRs welcome at PrepMasterLMS.