Tutify: Building a Scalable Education Platform with Dual Databases and a Complete Exam Engine

April 5, 2026 (4mo ago)

What is Tutify

Tutify is an education platform based in Singapore (tutify.com.sg) that helps students prepare for exams through structured assessments, gamification, and Math Olympiad competitions. I architected and built the entire backend and DevOps pipeline. The design and frontend were handled by another team.

Here's the scale of what I built:

What Numbers
MongoDB Models 48
Controllers 32
Route Files 16
Databases 2 (MongoDB Atlas + MySQL/MariaDB)
Frontend Apps Served 2 (admin panel + student app)
Validation Code 19,000+ lines (express-validator)
Third-Party Integrations 6+ (Stripe, SendGrid, AWS SES, DigitalOcean, etc.)

Tech Stack

Layer What's Used
Runtime Node.js, Express.js
Databases MongoDB Atlas (Mongoose), MySQL/MariaDB (Sequelize)
Cache Redis
Auth JWT, Passport.js (Google OAuth, Facebook OAuth)
Payments Stripe (subscriptions, scheduling, promo codes)
File Storage DigitalOcean Spaces (S3-compatible CDN)
Email SendGrid + AWS SES
PDF Generation html-pdf + pdf-lib
Background Jobs node-cron
Validation express-validator (19,000+ lines)
DevOps PM2, GitLab CI/CD

Why Two Databases

This is probably the first question anyone would ask, so let me explain.

Tutify's data falls into two very different categories. On one side, you have content like exam papers, question banks, student profiles, and curriculum structures. These are deeply nested, vary in shape from one question type to another, and change frequently. MongoDB is perfect for this. A single MCQ question document might have answer choices, images, difficulty tags, and curriculum metadata all embedded together. A free-response question looks completely different. Forcing all of that into rigid SQL tables would have been painful.

On the other side, you have exam responses. When a student finishes an exam, I need to run analytics: how did all students in Level 5 perform on Algebra questions this month? Which topics have the lowest pass rates? These are classic relational queries with joins across students, exams, papers, and responses. MySQL handles this far better than MongoDB's aggregation pipeline would.

So the split is simple: MongoDB for flexible content, MySQL for structured analytics. Each database does what it's best at, and neither is fighting against its nature.


The Assessment Engine

This was the most complex part of the system. Tutify needed a complete exam lifecycle, from question creation all the way through to graded results.

Five-Level Curriculum Taxonomy

Every question in the system lives inside a five-level hierarchy:

Academic Level > Subject > Strand > Topic > Question

For example: Primary 5 > Math > Numbers and Algebra > Fractions > "What is 3/4 + 1/2?"

This taxonomy drives everything. Teachers can build exams by picking a topic and pulling questions from the bank. Analytics roll up from individual questions to topics to strands and all the way to academic levels. The curriculum structure itself is admin-configurable, so when Singapore's MOE updates their syllabus, the platform adapts without code changes.

Question Verification Pipeline

Raw questions don't go straight into exams. Every question follows a three-stage pipeline:

NEW > PENDING > VERIFIED

A teacher creates a question (NEW), submits it for review (PENDING), and a senior reviewer approves or rejects it (VERIFIED or back to NEW with feedback). Only VERIFIED questions can appear in live exams. This keeps quality consistent across the question bank.

Multi-Paper Exams with Independent Configs

A single exam can contain multiple papers, and each paper has its own configuration: time limit, total marks, question mix, and passing score. A math exam might have Paper 1 (MCQ, 30 minutes, auto-graded) and Paper 2 (free-response, 60 minutes, manually reviewed). Each paper runs independently.

Timed Delivery with Pause and Resume

This is where things got interesting. Students need real exam pressure with a ticking clock, but they also need the ability to pause. The server tracks pause timestamps, not just elapsed time. When a student pauses, I record the timestamp. When they resume, I calculate how much time has actually passed. The remaining time is always computed server-side so students can't manipulate it by changing their system clock.

Auto-Grading and Manual Review

MCQ answers are graded instantly on submission. The system compares selected answers against the verified answer key and calculates scores automatically. Free-response questions go into a review queue where teachers grade them manually, add comments, and release scores. The exam isn't marked as complete until all papers, both auto-graded and manually reviewed, are finalized.

Bulk Question Import

Building a question bank one question at a time is tedious. I built an Excel (XLS) import pipeline where teachers can upload a spreadsheet with hundreds of questions, each mapped to the five-level taxonomy. The importer validates every row, flags errors, and creates question documents in bulk. This alone probably saved the content team hundreds of hours.


Points and Gamification

Tutify uses a gamification system to keep students motivated. Points are awarded for completing exams, submitting answers, referring friends, and claiming bonus activities. The critical design decision was making every single point change go through one centralized service.

// Centralized points service - every point change goes through here
async addPoints(mmosUserId, amount, transactionType, metadata) {
  const user = await MmosUser.findById(mmosUserId);
  const beforeScore = user.score;
  const afterScore = Math.max(0, beforeScore + amount);
  user.score = afterScore;
  await user.save();
  await PointsLog.create({
    mmosUserId, transactionType, amount,
    beforeScore, afterScore, metadata, timestamp: new Date()
  });
}

Every call captures a before/after snapshot and writes an audit log. If there's ever a dispute about a student's score, I can trace every single point change back to its source.

The system handles seven transaction types:

Transaction Type When It Happens
exam_submit Student submits a completed exam
exam_attempt Student starts an exam attempt
bonus_claim Student claims an approved bonus activity
referral New user signs up with a referral code
referrer_bonus Referred user hits a score threshold, referrer rewarded
manual_adjustment Admin manually adjusts a student's score
deduction Points removed (penalties or corrections)

Referral Bonus Cascade

The referral system has a fun twist. When a new user signs up with a referral code, the referrer gets a small bonus. But there's also a cascade: when that referred user reaches a certain score threshold, the referrer gets a second, larger bonus. This encourages referrers to actually help the people they invite, not just spam links.

Duplicate Prevention

A subtle but important detail: every exam attempt and submission has boolean flags (attemptPointsAwarded, submitPointsAwarded) that prevent double-awarding. If a network glitch causes a retry, or if a student somehow triggers a double submission, the system checks the flag before awarding points. No duplicates, ever.


Three-Tier Authorization

Most apps have two levels of auth: "are you logged in?" and "are you an admin?" Tutify needed more granularity than that.

Every API request passes through three gates:

  1. JWT Verification. Is the token valid and not expired? This catches unauthenticated requests.
  2. Role-Based Access. Is this user a student, teacher, or admin? Each role has different permissions.
  3. Module and Permission-Based Access. Does this specific role have access to this specific module, for this specific HTTP method?

The third tier is the interesting one. The admin panel has modules like "Question Bank," "Exam Management," "User Management," and so on. Each admin role can be granted read, write, or delete access to specific modules. So a content reviewer might have read/write access to the Question Bank but no access to User Management. The system maps HTTP methods to permissions: GET requires read access, POST/PUT requires write access, DELETE requires delete access.

This is enforced by 19,000+ lines of express-validator rules. Every route has explicit validation for every field, every parameter, every query string. Nothing gets through without being checked.


MMOS Competition System

MMOS (Math and Mind Olympiad Singapore) is a Math Olympiad competition built into the platform. It's a separate system from regular exams with its own data model.

The competition supports events (a specific Olympiad session), teams (groups of students competing together), groups (divisions by level or age), and leaderboards. Students compete individually and as teams, with scores rolling up to team totals. Leaderboards update in real time as results come in.

The tricky part was making MMOS work alongside the regular exam system without duplicating code. Both systems share the question bank and taxonomy, but MMOS adds competition-specific logic like team management, event scheduling, and ranked leaderboards.


Stripe Subscription System

Tutify runs on a subscription model with multiple plan tiers. Here's what the billing system handles:

Plans and Trials. Each plan has a price, billing cycle, and optional trial period. Students can try the platform before committing.

Mid-Cycle Plan Changes. This is where Stripe's Subscription Schedule API earned its keep. When a student upgrades or downgrades mid-cycle, the system uses Stripe's schedule API to handle prorations and timing correctly. No custom billing math needed.

Promo Codes. Admins can create discount codes with limits on usage count, expiration dates, and applicable plans.

Renewal Reminders. A cron job runs daily at 10 AM SGT and checks for subscriptions expiring in the next 7 days. If a subscription is about to lapse, the student gets an email reminder. Getting the timezone right was important since the server runs in UTC but the business operates in Singapore (UTC+8).


Bonus Activities and Approval Workflow

Beyond exams, students can earn points through bonus activities: watching educational videos, completing practice sets, attending workshops. The workflow goes like this:

  1. Student completes an activity and submits a claim with screenshot evidence
  2. The claim enters a review queue
  3. An admin reviews the evidence and approves or rejects the claim
  4. If approved, points are awarded through the centralized points service (with full audit trail)
  5. If rejected, the student gets a reason and can resubmit

This keeps the system honest. Students can't just click "I did it" and collect points.


Background Jobs

A daily cron job runs at 10 AM SGT (2 AM UTC) to check subscription renewals. Getting timezone handling right in a cron-based system was more nuanced than I expected. The server runs in UTC, but "10 AM Singapore time" shifts depending on whether you're calculating in server time or local time. I used explicit timezone conversion so the job always fires at the right local time, regardless of the server's timezone setting.

Other scheduled jobs handle things like cleaning up expired exam sessions and sending reminder emails.


What I Built

Here's a summary of everything the backend delivers:

Area What It Does
Dual-Database Architecture MongoDB for flexible content, MySQL for relational analytics
Exam Lifecycle Engine 5-level taxonomy, verification pipeline, timed delivery, auto-grading
Gamification System Centralized points service, 7 transaction types, full audit logging
Three-Tier Auth JWT + role-based + module/permission-based with method mapping
MMOS Competitions Events, teams, groups, leaderboards for Math Olympiad
Stripe Billing Plans, trials, schedule API, promo codes, renewal reminders
Bonus Activities Submit-review-approve workflow with evidence and audit trail
Background Jobs Timezone-aware cron jobs for renewals and maintenance
API Scale 32 controllers, 48 models, 16 route files serving 2 frontends
Validation 19,000+ lines of express-validator rules
DevOps GitLab CI/CD pipeline with PM2 process management
Integrations Stripe, SendGrid, AWS SES, DigitalOcean Spaces, Google/Facebook OAuth

What I Took Away

  1. The dual-database decision was the best architectural call I made. Trying to force everything into one database would have made either the content system or the analytics system suffer.
  2. Building a complete exam engine taught me how many edge cases exist in timed, stateful workflows. Pause/resume alone had a dozen scenarios I hadn't considered upfront.
  3. A centralized points service with audit logging sounds like over-engineering until the first time someone disputes their score. Then it's the most valuable thing in the system.
  4. Three-tier authorization is more work to set up, but it makes the admin panel genuinely flexible. Adding a new admin role with specific permissions takes minutes, not code changes.
  5. 19,000 lines of validation is a lot. But every line exists because of a real input that could go wrong. Validation is not glamorous, but it's what keeps a production system stable.