Back to Blog
Case Study
Next.js 15
FastAPI
MongoDB

Building NCEATutor NZ: A Full-Stack Ed-Tech Platform

15 min read

How we built a production-grade NCEA tutoring marketplace from concept to deployment in 1 months, achieving 86% performance improvements through MongoDB aggregation pipelines and Next.js optimisation.

86%
Faster Queries
100%
Security Score
1
Months to Launch
10+
Technologies

The Challenge

When we set out to build NCEATutor NZ, we knew we were tackling more than just another marketplace. We needed to create a platform that could match students with tutors based on complex NCEA level requirements, handle secure payments, manage document uploads, and do it all while maintaining sub-200ms query times.

The education technology space demands reliability, security, and performance. Parents and students need confidence that their data is protected, payments are secure, and tutors are qualified. We couldn't compromise on any of these requirements.

Technical Architecture

Frontend Stack

  • Next.js 15.0.3 with App Router for optimal performance and SEO
  • React 18.3.1 with concurrent features and automatic batching
  • TypeScript 5.7.2 for type safety and better developer experience
  • Tailwind CSS with shadcn/ui for consistent, accessible components
  • React Hook Form + Zod for performant form validation

Backend Stack

  • FastAPI (Python 3.12+) for high-performance async API endpoints
  • MongoDB with Motor (async driver) for flexible document storage
  • Stripe Checkout + Webhooks for secure payment processing
  • Cloudflare R2 for cost-effective document storage

Security Layer

  • bcrypt with 12 salt rounds for password hashing
  • JWT tokens with 7-day expiry and HTTP-only cookies
  • CSRF protection with HMAC signatures on sensitive operations
  • Rate limiting with endpoint-specific throttling (5-20 req/min)

The 86% Performance Improvement

The Problem

Our initial implementation fetched all tutors from the database and sorted them in memory. This worked fine with 50 tutors during development, but we knew it would collapse under real-world load.

❌ Before: In-Memory Sorting
# Fetch ALL tutors (could be thousands)
tutors = await db.tutors.find(filters).to_list(None)

# Sort in Python (expensive!)
sorted_tutors = sorted(
    tutors,
    key=lambda t: t['subjects_count'],
    reverse=True
)

# Query time: 855ms at 500 tutors

The Solution: MongoDB Aggregation Pipelines

We moved all sorting, filtering, and pagination logic into MongoDB aggregation pipelines. This leveraged MongoDB's powerful indexing and query optimisation, pushing computation closer to the data.

✅ After: MongoDB Aggregation Pipeline
pipeline = [
    # Filter documents in database
    {"$match": filters},

    # Add computed fields efficiently
    {"$addFields": {
        "subjects_count": {"$size": "$subjects"},
        "rating_count": {"$size": "$ratings"}
    }},

    # Sort using indexes
    {"$sort": {"subjects_count": -1, "rating": -1}},

    # Paginate at database level
    {"$skip": skip},
    {"$limit": limit}
]

tutors = await db.tutors.aggregate(pipeline).to_list(None)

# Query time: 120ms at 500 tutors
# 86% improvement!

Compound Indexes for Maximum Speed

We created strategic compound indexes to support our most common query patterns:

# Location + Subject + Availability
db.tutors.createIndex({
    "location": 1,
    "subjects": 1,
    "is_available": 1
})

# Rating + Subject Count (for sorting)
db.tutors.createIndex({
    "rating": -1,
    "subjects_count": -1
})

💡 Key Takeaway

Moving computation from application code to the database reduced query time from 855ms to 120ms—a transformative improvement that makes the platform feel instant for users.

The NCEA Level Ladder Algorithm

One of the unique challenges in building an NCEA tutoring platform is handling the complexity of New Zealand's three-level qualification system. A Level 3 student might need help with Level 1 Chemistry but Level 3 Calculus—we needed intelligent matching.

How It Works

  1. Student specifies required subjects

    e.g., "Level 2 Physics" and "Level 3 Calculus"

  2. Algorithm searches upward through levels

    Level 2 Physics → match tutors with Level 2 OR Level 3 Physics

  3. Tutors ranked by qualification match

    Exact level match > Higher level > General subject expertise

  4. Results filtered by availability and location

    Online/In-person preferences, Auckland regions, time slots

🎯 Matching Logic Example
# Student needs: Level 2 Physics

# Tutor A: Level 2 Physics ✅ (exact match, score: 100)
# Tutor B: Level 3 Physics ✅ (higher level, score: 95)
# Tutor C: Level 1 Physics ❌ (lower level, no match)
# Tutor D: Physics (general) ✅ (general, score: 80)

# Final ranking: A → B → D

Comprehensive Security

Education platforms handle sensitive data—student information, payment details, personal documents. We implemented multiple layers of security:

Authentication

  • • bcrypt with 12 salt rounds
  • • JWT with 7-day expiry
  • • HTTP-only cookies
  • • Secure session management

API Protection

  • • CSRF tokens on mutations
  • • Rate limiting per endpoint
  • • Input validation with Zod
  • • CORS configuration

Payment Security

  • • Stripe Checkout (PCI compliant)
  • • Webhook signature verification
  • • No card data stored locally
  • • Transaction logging

Data Protection

  • • Document access control
  • • Signed URLs for file access
  • • Privacy Act 2020 compliance
  • • Data retention policies

⚠️ Security Best Practice

We implemented rate limiting with different thresholds per endpoint: 5 req/min for authentication, 10 req/min for search, 20 req/min for profile updates. This prevents abuse while maintaining good UX.

Payment Processing with Stripe

We chose Stripe Checkout for its security, reliability, and excellent developer experience. The platform handles all payment complexity while we focus on the tutoring experience.

Payment Flow

  1. Student books session

    Selects tutor, date, time, and session type (online/in-person)

  2. Backend creates Stripe Checkout session

    Includes metadata: tutor_id, student_id, session_details

  3. User redirected to Stripe's secure page

    No payment data touches our servers (PCI compliance)

  4. Webhook confirms payment

    We verify signature, create booking, notify both parties

  5. User redirected to confirmation page

    Session details, calendar invite, tutor contact info

💳 Webhook Signature Verification
@app.post("/webhook/stripe")
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig_header = request.headers.get("stripe-signature")

    try:
        # Verify webhook signature
        event = stripe.Webhook.construct_event(
            payload, sig_header, WEBHOOK_SECRET
        )

        if event["type"] == "checkout.session.completed":
            session = event["data"]["object"]

            # Create booking in database
            await create_booking(
                tutor_id=session.metadata["tutor_id"],
                student_id=session.metadata["student_id"],
                amount=session.amount_total,
                stripe_session_id=session.id
            )

        return {"status": "success"}
    except Exception as e:
        return JSONResponse(status_code=400, content={"error": str(e)})

Lessons Learned

1. Optimise at the Database Level First

Before adding caching layers or complex application logic, we extracted maximum performance from MongoDB. Aggregation pipelines and proper indexing gave us 86% improvement—far more than any application-level optimisation could achieve.

2. Security Isn't an Afterthought

We built authentication, CSRF protection, and rate limiting from day one. Retrofitting security into an existing codebase is exponentially harder than designing it in from the start.

3. TypeScript + Zod = Fewer Bugs

Type safety on the frontend (TypeScript) combined with runtime validation (Zod schemas) caught dozens of bugs before they reached production. The investment in types pays dividends in reliability.

4. Next.js 15 App Router is Production-Ready

We built the entire frontend with App Router and React Server Components. The performance benefits (automatic code splitting, server-side rendering) and developer experience (file-based routing, layouts) exceeded expectations.

5. Stripe Handles Payment Complexity

Using Stripe Checkout meant we never touched payment card data, automatically got PCI compliance, and could support multiple payment methods with minimal code. The webhook system is reliable and well-documented.

Results

855ms → 120ms
Query Performance

MongoDB aggregation pipelines reduced search query time by 86%, making the platform feel instant.

0 Breaches
Security Track Record

Comprehensive security layers (bcrypt, JWT, CSRF, rate limiting) have kept the platform secure since launch.

1 Months
Development Timeline

From initial concept to production deployment with full payment processing and document management.

Complete Tech Stack

Frontend

  • • Next.js 15.0.3 (App Router)
  • • React 18.3.1
  • • TypeScript 5.7.2
  • • Tailwind CSS + shadcn/ui
  • • React Hook Form + Zod
  • • Lucide Icons

Backend

  • • FastAPI (Python 3.12+)
  • • MongoDB + Motor
  • • Stripe API (Checkout + Webhooks)
  • • Cloudflare R2 Storage
  • • bcrypt + JWT
  • • CSRF + Rate Limiting

Need a Custom Ed-Tech Platform?

We build production-grade web applications with modern tech stacks. From concept to deployment, we handle the complexity so you can focus on your business.

Check out the live platform:

Visit NCEATutor NZ