// chapter 1
The problem
Before writing code I reviewed the legacy exam system and listed what must never happen again: timers that reset on refresh, practice papers scored with a different model from live ones, the same data duplicated across many tables, and audit fields filled with fixed values.
The institute needed exams that were fair (the clock can’t be gamed), reliable (no lost answers on a bad connection), bilingual (English and Hindi) and auditable (who changed what, and when).
// chapter 2
What I built
Papers
Papers built against the real course and subject catalogue; questions imported from Excel in English and Hindi with whole-file validation; publish, unpublish, copy to another course; instructions and a timetable per course.
Taking the exam
Begin and resume; every answer autosaved; a deadline set by the server, with a sweep that auto-submits within 60 seconds of time running out; proctoring flags recorded for review.
Running exam day
A live status view of every student with Excel export, extra time for one student, and cancelling an attempt so a student can re-sit.
Results
MCQs scored automatically; practical marks uploaded by Excel; results declared now, scheduled or withdrawn; changes go through propose-and-approve revisions; full results in Excel.
Access and audit
Separate staff logins; students sign in through the institute’s existing system; per-paper student lists; an append-only log of every admin change and login.
Mobile app
An API and handover for the student app: token exchange from the main app, a timer based on the server’s clock, a retry queue for answers on weak connections.
// chapter 3
How an attempt works
The device never decides time or score. It shows a countdown from the server’s deadline and sends answers; the server scores them and stores results when an admin declares them.
// chapter 4
The hard parts
Hundreds of students behind one address. The exam server calls the institute’s main API for every student, so all of them looked like one visitor and hit its rate limit. I added a signed service key for server-to-server calls, compared in constant time, and limited the exam API per student account instead of per IP. Tested live: 200 calls with the key went through; without it, requests were blocked after 120.
Students with more than one record. The legacy data had several records sharing one phone number. Attempts, blocks and limits are matched on the enrolment number, so each person’s exam always lands on the right record.
Results as a stored snapshot. Declared results are saved with the labels of that moment, so later catalogue edits can’t silently change a past result, and the table is ready to sync back to the main system.
Exam-day safety net
Daily database backups copied to cloud storage and checked for completeness, plus an audit after each exam day for off-list entries, double attempts and wrong enrolment numbers.
// chapter 5
Results
937
attempts, all submitted, across 24 live papers
5 days
of live exams, about 190 students a day
0
double attempts or off-list entries in the audits of the last four exam days
- Node.js
- Fastify
- Prisma
- MySQL
- React
- Vite
- Flutter
- Excel import / export
- PM2
- Nginx