Architecture
System Architecture
A complete technical breakdown of how AEGIS ExamLab is structured — from its Next.js routing and MongoDB data model to the security chain, heartbeat telemetry, and middleware firewall.
Repository Directory Structure
├── src/
│ ├── app/ # Next.js App Router root
│ │ ├── (dashboard)/ # Route group — all admin pages
│ │ │ ├── dashboard/pcs/
│ │ │ ├── dashboard/students/
│ │ │ ├── dashboard/questions/
│ │ │ ├── dashboard/exams/
│ │ │ ├── dashboard/live-status/
│ │ │ ├── dashboard/results/
│ │ │ └── dashboard/logs/
│ │ ├── exam/[examId]/ # Student exam interface (dynamic route)
│ │ ├── login/ # Admin login page
│ │ ├── api/ # Utility API routes (debug, migrations)
│ │ └── page.tsx # Root: PC Registration portal
│ ├── components/ # Radix UI + shadcn/ui component library
│ ├── hooks/ # Custom React hooks (use-toast)
│ ├── lib/
│ │ ├── actions.ts # ALL Server Actions — business logic
│ │ ├── mongodb.ts # DB connection + collection helpers
│ │ ├── types.ts # TypeScript type definitions
│ │ └── utils.ts # Tailwind class utilities
│ └── middleware.ts # Edge routing firewall
├── docs/ # GitHub Pages documentation (this site)
├── scripts/seed-db.js # Admin credential bootstrapper
├── database-fix.js # MongoDB index repair utility
├── Dockerfile # Multi-stage production image
└── .env.example # Environment variable template
MongoDB Collections
pcs
Approved and pending client machines
pc_requests
Unapproved registration requests
students
Student roster with exam assignments
questions
Question bank with AI metadata
exams
Exam configurations and state
exam_results
Immutable submission records
admins
Admin credentials
admin_logs
Full audit trail
Server-Side Validation Chain
Before any exam data is served or any submission is accepted, the request must pass through a strict sequential trust chain. Each step references the previous, making it impossible to bypass one level by knowing another's identifiers.
1. PC Exists & Approved
Lookup by pcIdentifier → status === 'Approved'
2. Student Mapping Verified
PC's assignedStudentId === provided studentId
3. Exam Assignment Verified
Student's assignedExamId === provided examId
4. Exam In Progress
Exam status === 'In Progress'
5. No Prior Submission
No document in exam_results for this student+exam
✓ Access Granted
Middleware Routing Firewall
The src/middleware.ts Edge function intercepts all matching requests before Next.js renders a single component. This ensures the auth check has zero overhead from React rendering.
| Route Pattern | Auth Check | Rationale |
|---|---|---|
| / | Exempt | PC registration is public by design — any machine must be able to self-register. |
| /login | Exempt | Required to be publicly accessible for login flow. |
| /exam/* | Exempt (token-based) | Exam pages use localStorage device tokens verified by Server Actions, not HTTP cookies. |
| /dashboard/* | Required | Checks for auth cookie. Redirects to /login if absent. |
Heartbeat Telemetry System
AEGIS uses HTTP long-polling over WebSockets for telemetry. This is a deliberate architectural choice.
Why HTTP Polling over WebSockets?
Institutional networks (universities, examination halls) frequently operate behind strict Layer-7 firewalls that block WebSocket upgrade requests. HTTP polling (standard GET/POST) is virtually never blocked. A 15-second interval provides near-real-time visibility without significant server overhead for typical exam sizes (10–100 concurrent PCs).
The client's useEffect in src/app/exam/[examId]/page.tsx sets up a setInterval that fires updatePcLiveStatus(pcIdentifier, 'Attempting') every 15,000ms. The cleanup function (return () => clearInterval(heartbeatInterval)) prevents memory leaks when the component unmounts.
Absolute Epoch Timer Synchronization
A common attack vector in web-based exams is manipulating the local system clock to extend the countdown timer. AEGIS defends against this architecturally:
const examEndTime = new Date(data.exam.startTime).getTime()
+ data.exam.duration * 60 * 1000;
const now = new Date().getTime();
const remainingTime = Math.max(0, Math.floor((examEndTime - now) / 1000));
The countdown is computed from the server-provided absolute epoch timestamps (startTime + duration in minutes). Even if a student manipulates their clock, the server's own status field governs submission eligibility — once the server marks the exam Completed, getExamDetails rejects all requests.
MongoDB Connection Pooling
Next.js in production spawns multiple Node.js worker processes. Each cold-started serverless function would naively open a new MongoDB connection, rapidly exhausting the cluster's connection limit (typically 500 on free-tier Atlas). AEGIS solves this with a module-level cache:
let cachedClient: MongoClient | null = null;
export async function getDb() {"{"}
if (cachedClient) return cachedClient.db(); // ← reuse existing
await client.connect();
cachedClient = client;
return client.db();
{"}"}
The cachedClient is stored in module scope. In Next.js, module scope persists across multiple requests within the same Node.js process, effectively creating a connection pool per worker without needing an external pooling library.