A self-hosted, secure MCQ examination platform built with Next.js 16, PostgreSQL, and Prisma ORM.
HI Tech Examination enables administrators to create, manage, and conduct secure MCQ examinations with access control, time limits, full-screen enforcement, and email notifications β all self-hosted. Unlike Google Forms, it is purpose-built for examinations.

| Feature | Description |
|---|---|
| JWT Authentication | Secure login with access + refresh token rotation (HTTP-only cookies) |
| Email Verification | SMTP-based verification required before admin access is granted |
| Password Recovery | Forgot/reset password flow with token-based reset links |
| Exam Builder | Create MCQ exams with 4-option questions, drag-and-drop reordering |
| Exam Settings | Time limits, shuffle questions/options, passing score, scheduled start/end dates |
| Access Control | Public link (anyone with link) or specific Gmail whitelist per exam |
| Publish Management | Draft β Published β Closed lifecycle control |
| Submissions Viewer | View all student responses, per-student detailed results |
| Analytics Dashboard | Total attempts, average score, pass rate visualized with Recharts |
| Audit Logging | Track admin actions for accountability |
| Email Notifications | Receive submission notifications via SMTP (Nodemailer) |
| Feature | Description |
|---|---|
| No Registration Required | Students identified by Gmail only, auto-created on first exam access |
| One-Session Enforcement | Each student can attempt an exam only once (DB-level unique constraint) |
| Full-Screen Mode | Enforced on exam start with exit prevention and logging |
| Tab Switch Detection | Detected, logged, and configurable warning thresholds |
| Time Limits + Auto-Submit | Countdown timer; exam auto-submits on expiry |
| Exit Prevention | beforeunload warning; auto-submit on close attempt |
| Immediate Results | Score displayed on submission (if enabled by admin) |
| Email Confirmation | Students receive acknowledgment/result emails via SMTP |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client β
β ββββββββββββββββ βββββββββββββββββββββββββββ β
β β Admin Panel β β Exam Taking (Public) β β
β β /admin/** β β /exam/[slug] β β
β ββββββββ¬ββββββββ ββββββββββββ¬βββββββββββββββ β
βββββββββββΌβββββββββββββββββββββββββββββΌβββββββββββββββββββ
β β
βββββββββββΌβββββββββββββββββββββββββββββΌβββββββββββββββββββ
β βΌ Next.js App Router βΌ β
β ββββββββββββββββ βββββββββββββββββββββββββββ β
β β Admin API β β Exam API β β
β β /api/admin/ β β /api/exam/ β β
β β (JWT guard) β β (session token) β β
β ββββββββ¬ββββββββ ββββββββββββ¬βββββββββββββββ β
β β β β
β ββββββββββββββββ¬ββββββββββββββ β
β βΌ β
β βββββββββββββββββββ β
β β Prisma ORM β β
β β (PostgreSQL) β β
β βββββββββββββββββββ β
β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Nodemailer SMTP β β Rate Limiting / Audit β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Layer | Technology |
|---|---|
| Framework | Next.js 16.2.3 (App Router) |
| Language | TypeScript 5 (strict mode) |
| React | React 19.2.4 |
| Styling | Tailwind CSS v4, shadcn/ui |
| Database | PostgreSQL |
| ORM | Prisma 7.7.0 |
| Auth | JWT (jsonwebtoken) + bcryptjs |
| Validation | Zod v4 |
| Nodemailer | |
| State | Zustand |
| Drag & Drop | dnd-kit |
| Charts | Recharts |
| Icons | Lucide React |
| Notifications | Sonner |
examforge/
βββ app/ # Next.js App Router
β βββ admin/ # Admin panel pages
β β βββ dashboard/ # Dashboard (exams, settings)
β β βββ login/ # Admin login
β β βββ register/ # Admin registration (invite-only)
β β βββ forgot-password/ # Password recovery
β β βββ reset-password/ # Password reset
β β βββ verify-email/ # Email verification
β βββ api/ # API route handlers
β β βββ admin/ # Admin API (JWT-protected via withAdminAuth)
β β βββ exam/ # Public exam API (session-validated via withExamSession)
β βββ exam/[slug]/ # Public exam entry, taking, and result
β βββ layout.tsx # Root layout (Geist fonts)
β βββ page.tsx # Landing page
β βββ globals.css # Tailwind v4 + CSS variable theme
βββ components/
β βββ admin/ # Admin-specific components
β β βββ AccessControl.tsx # Exam access rule configuration
β β βββ AuthProvider.tsx # Admin auth context provider
β β βββ DashboardNav.tsx # Dashboard navigation sidebar
β β βββ ExamForm.tsx # Exam creation/editing form
β β βββ QuestionEditor.tsx # Question & option editor (dnd-kit)
β β βββ SubmissionsTable.tsx # Submissions list viewer
β βββ exam/ # Exam-taking components
β β βββ ExamEntry.tsx # Gmail verification entry
β β βββ ExamTimer.tsx # Countdown timer display
β β βββ FullscreenGuard.tsx # Full-screen enforcement
β β βββ QuestionCard.tsx # Question display & option selection
β β βββ ResultCard.tsx # Score result display
β βββ ui/ # Shadcn UI primitives (~60 components)
βββ hooks/
β βββ use-mobile.ts # Responsive breakpoint hook
βββ lib/
β βββ generated/prisma/ # Auto-generated Prisma client
β βββ validators/ # Zod schemas (access, auth, exam)
β βββ apiClient.ts # Frontend API client with auto-refresh
β βββ auth.ts # JWT sign/verify, token hashing
β βββ withAdminAuth.ts # Admin route handler wrapper
β βββ withExamSession.ts # Exam session handler wrapper
β βββ prisma.ts # Prisma singleton (dev hot-reload safe)
β βββ mailer.ts # Nodemailer transporter configuration
β βββ email-templates/ # HTML email template definitions
β βββ audit.ts # Audit logging helpers
β βββ rateLimit.ts # In-memory rate limiting middleware
β βββ slugify.ts # URL slug generation utility
β βββ utils.ts # cn() utility (clsx + tailwind-merge)
βββ prisma/
β βββ schema.prisma # Database schema (15+ models)
β βββ seed.ts # Seed script for initial super admin
β βββ migrations/ # Prisma SQL migrations
βββ public/ # Static assets
- Node.js 20+
- PostgreSQL 14+ (running and accessible)
- SMTP credentials (Gmail App Password, SendGrid, etc. β for email features)
git clone https://github.com/your-username/examforge.git
cd examforge
npm installCopy the example environment file and fill in your values:
cp .env.example .env.local| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@localhost:5432/examforge |
JWT_ACCESS_SECRET |
JWT access token signing key (β₯32 chars) | your_super_secret_key_32_chars_min |
JWT_REFRESH_SECRET |
JWT refresh token signing key (β₯32 chars) | another_super_secret_key_32_chars |
JWT_ACCESS_EXPIRES_IN |
Access token expiry | 15m |
JWT_REFRESH_EXPIRES_IN |
Refresh token expiry | 7d |
SMTP_HOST |
SMTP server host | smtp.gmail.com |
SMTP_PORT |
SMTP server port | 587 |
SMTP_USER |
SMTP username | you@gmail.com |
SMTP_PASS |
SMTP password (App Password for Gmail) | abcd efgh ijkl mnop |
NEXT_PUBLIC_APP_URL |
Public app URL | http://localhost:3000 |
SEED_ADMIN_EMAIL |
Initial super admin email | admin@yourdomain.com |
SEED_ADMIN_PASSWORD |
Initial super admin password | ChangeMe@123 |
SEED_ADMIN_NAME |
Initial super admin display name | Super Admin |
# Generate Prisma client (output: lib/generated/prisma/)
npm run db:generate
# Run pending migrations
npm run db:migrate
# Seed the initial super admin account
npm run db:seednpm run devOpen http://localhost:3000 in your browser.
npm run build # Compile for production
npm run start # Start production server| Command | Description |
|---|---|
npm run dev |
Start development server with hot reload |
npm run build |
Production build (optimized) |
npm run start |
Start production server |
npm run lint |
Run ESLint |
npm run db:generate |
Regenerate Prisma client after schema changes |
npm run db:migrate |
Run database migrations (development) |
npm run db:seed |
Seed database (creates super admin via prisma/seed.ts) |
- Admin β JWT access tokens (short-lived, default 15 min) passed via
Authorization: Bearerheader. Refresh tokens (long-lived, default 7 days) stored as SHA-256 hashes in HTTP-only,SameSite=Strict,Securecookies. Token rotation on every refresh β old tokens are revoked. - Student β Session tokens (UUID) stored in
sessionStorage(neverlocalStorage). Validated server-side on every protected API call.
| Mechanism | Implementation |
|---|---|
| One session per student | DB unique constraint (exam_form_id, student_id) |
| Full-screen enforcement | requestFullscreen() API; exit events logged + warnings triggered |
| Tab/visibility detection | visibilitychange listener logs switches via heartbeat API |
| Exit prevention | beforeunload dialog triggers auto-save + warning |
| Time limit auto-submit | Client countdown + server-side heartbeat force-submits on expiry |
| Rate limiting | In-memory rate limiter on student-facing endpoints |
| Token hashing | All tokens (refresh, email verify, password reset) stored as SHA-256 hashes |
- All API inputs validated with Zod v4 schemas in
lib/validators/ - Prisma parameterized queries prevent SQL injection
- Sensitive fields (
passwordHash,tokenHash) never exposed in API responses - Email addresses normalized to lowercase before storage
| Model | Purpose |
|---|---|
Admin |
Admin accounts (invite-only, email-verified) |
Student |
Auto-created student records (Gmail-based, no password) |
ExamForm |
Exam definition, settings, scheduling, publishing |
ExamQuestion |
Questions belonging to an exam |
ExamQuestionOption |
Answer options per question |
ExamSession |
One session per student per exam with security tracking |
ExamResponse |
Student's response per question |
ExamResponseOption |
Selected options per response (multi-choice support) |
ExamAccessRule |
Access type per exam (public_link or specific_emails) |
ExamAllowedEmail |
Whitelisted emails for restricted-access exams |
RefreshToken |
JWT refresh token store (rotation-based, revocable) |
EmailNotification |
Outbound email queue log (pending β sent/failed) |
AuditLog |
Admin action audit trail |
- Source:
prisma/schema.prisma - Generated client:
lib/generated/prisma/(custom output path) - Client import: Always import from
@/lib/prisma(singleton, dev hot-reload safe)
Powered by Nodemailer over SMTP. Emails are queued in the EmailNotification table before dispatch with automatic retry (up to 3 attempts).
| Event | Recipient | Purpose |
|---|---|---|
| Admin registered | New admin | Email verification link |
| Admin forgot password | Admin | Password reset link |
| Exam published (specific emails) | Allowed students | Exam invitation with link |
| Student submits exam | Student | Score / result confirmation |
| Student submits exam | Admin (exam owner) | New submission notification |
Email templates are defined in lib/email-templates/.
| Method | Route | Description |
|---|---|---|
POST |
/auth/register |
Create admin (by existing admin) |
POST |
/auth/login |
Login β access token + refresh cookie |
POST |
/auth/logout |
Revoke refresh token |
POST |
/auth/refresh |
Rotate refresh token |
POST |
/auth/verify-email |
Verify email token |
POST |
/auth/forgot-password |
Send password reset email |
POST |
/auth/reset-password |
Reset password with token |
GET |
/me |
Current admin profile |
GET/POST |
/exams |
List / create exams |
GET/PATCH/DELETE |
/exams/[id] |
Read / update / archive exam |
PATCH |
/exams/[id]/publish |
Toggle published state |
CRUD |
/exams/[id]/questions |
Manage questions & options |
PATCH |
/exams/[id]/questions/reorder |
Reorder questions |
GET/PATCH |
/exams/[id]/access |
Manage access rules |
POST/DELETE |
/exams/[id]/access/emails |
Manage allowed emails |
GET |
/exams/[id]/submissions |
List all submissions |
GET |
/exams/[id]/analytics |
Pass rate, average score stats |
| Method | Route | Description |
|---|---|---|
GET |
[slug] |
Get exam info (if published & within schedule) |
POST |
[slug]/verify-access |
Validate Gmail β create/return session |
GET |
[slug]/questions |
Get questions (requires session token) |
POST |
[slug]/response |
Save answer for one question |
POST |
[slug]/submit |
Finalize exam, calculate score, send emails |
POST |
[slug]/heartbeat |
Update security counters, check expiry |
GET |
[slug]/result/[sessionId] |
Fetch result after submission |
Built with shadcn/ui (Radix primitives) and Tailwind CSS v4.
| Category | Components |
|---|---|
| Form | Button, Input, Label, Textarea, Select, Checkbox, RadioGroup, Switch, Calendar, Form |
| Overlay | Dialog, Alert, Toast (Sonner), Tooltip, Popover, Dropdown Menu, Context Menu, Sheet, Drawer |
| Navigation | Tabs, Breadcrumb, Pagination, Command Palette (cmdk) |
| Layout | Card, Separator, Scroll Area, Resizable Panels, Carousel |
| Data | Table, Badge, Progress, Chart (Recharts) |
| Feedback | Skeleton, Spinner, Avatar |
Dark mode supported via next-themes. The cn() utility from @/lib/utils handles conditional class merging (clsx + tailwind-merge).
- Strict mode enabled (
strict: true) - Path alias:
@/*β project root - Target: ES2017, module: ESNext
- Admin routes wrapped with
withAdminAuth()β providesadminIdin context - Exam routes wrapped with
withExamSession()β validates active session - Request bodies validated with Zod schemas in
lib/validators/
- Login β server issues access token (15 min) + sets refresh token cookie (7 days)
- Access token passed via
Authorization: Bearerheader - On 401:
apiFetch()in@/lib/apiClient.tsautomatically refreshes (singleton in-flight β concurrent 401s share one refresh call) - On refresh: old token revoked, new pair issued (rotation)
import { apiFetch } from "@/lib/apiClient";
// Example: create an exam
const response = await apiFetch("/api/admin/exams", {
method: "POST",
body: JSON.stringify({ title: "Midterm 2026", timeLimitMinutes: 60 }),
});- Multiple-choice question scoring (partial credit)
- Bulk student import via CSV
- Exam result export (CSV/PDF)
- Admin role-based permissions (editor vs viewer)
- IP-based access restrictions
- Browser lockdown integration (Respondus, Safe Exam Browser)
- Multi-language support (i18n)
- Docker deployment configuration
- Automated email queue processor (cron /
waitUntil)
This project is licensed under the MIT License.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
For issues, questions, or suggestions, please open an issue on GitHub.
Built with β€οΈ using Next.js, Prisma, and PostgreSQL.