Building NCEATutor NZ: A Full-Stack Ed-Tech Platform
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.
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.
# 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 tutorsThe 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.
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
- Student specifies required subjects
e.g., "Level 2 Physics" and "Level 3 Calculus"
- Algorithm searches upward through levels
Level 2 Physics → match tutors with Level 2 OR Level 3 Physics
- Tutors ranked by qualification match
Exact level match > Higher level > General subject expertise
- Results filtered by availability and location
Online/In-person preferences, Auckland regions, time slots
# 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
- Student books session
Selects tutor, date, time, and session type (online/in-person)
- Backend creates Stripe Checkout session
Includes metadata: tutor_id, student_id, session_details
- User redirected to Stripe's secure page
No payment data touches our servers (PCI compliance)
- Webhook confirms payment
We verify signature, create booking, notify both parties
- User redirected to confirmation page
Session details, calendar invite, tutor contact info
@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
MongoDB aggregation pipelines reduced search query time by 86%, making the platform feel instant.
Comprehensive security layers (bcrypt, JWT, CSRF, rate limiting) have kept the platform secure since launch.
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