Building ResultApp: Architecting a Multi-Tenant School SaaS for Nigeria
A deep dive into designing an isolated multi-tenant school result compilation platform, custom subdomain routing, and a frictionless credits-and-slots billing model.
The Problem: The Flaws of Subscriptions in Education SaaS
When designing school management software for Nigerian institutions, traditional \$30/month SaaS subscriptions fail for a simple reason: school operations run on distinct academic terms separated by extended holiday periods. Paying monthly for software during 3-month summer breaks creates churn and resentment.
For ResultApp, we threw out subscriptions entirely and designed a Credits & Slots Model:
- Slots: A school registers a finite number of active student slots for a term.
- Credits: Schools purchase unit credits that never expire. Generating or publishing a term result card consumes credits per student slot.
- If school is out of session, cost is zero.
Multi-Tenant Subdomain Routing with Next.js
Each registered school receives a custom subdomain (e.g. meritacademy.resultapp.org). We solved this with Next.js Edge Middleware:
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const url = req.nextUrl;
const hostname = req.headers.get("host") || "";
// Extract custom school subdomain
const currentHost = hostname.replace(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`, "");
if (currentHost && currentHost !== "www" && currentHost !== "app") {
// Rewrite path to multi-tenant school layout
return NextResponse.rewrite(new URL(`/schools/${currentHost}${url.pathname}`, req.url));
}
return NextResponse.next();
}This rewrite happens at the CDN edge before server rendering, providing zero-latency subdomain resolution.
Database Tenant Isolation & Row Level Security
Data leakage between competing schools is fatal. While schema-per-tenant works for enterprise, maintaining hundreds of distinct Postgres schemas introduces migration nightmares.
Instead, we adopted a unified relational database with Postgres Row Level Security (RLS):
ALTER TABLE exam_scores ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON exam_scores
FOR ALL
USING (school_id = current_setting('app.current_school_id')::uuid);Every database transaction sets app.current_school_id before querying. Even if an engineer writes a query without a WHERE school_id = ... clause, Postgres guarantees that no data from another school is accessible.
Automated Tamper-Proof Report Card Generation
Nigerian parents demand high-resolution printable report cards with verifiable QR codes and school crest watermarks.
Instead of running heavy Puppeteer instances that exhaust server memory, we implemented high-speed vector SVG compositing on the client and serverless PDF rasterization with @react-pdf/renderer.
- Result compilation: Instant client-side grade weighting and rank calculation.
- Verification: Each generated card includes a cryptographically signed HMAC token in the QR code, allowing instant verification by visiting
resultapp.org/verify?token=....
Conclusion & Lessons Learned
- Design billing for real customer cash flows, not Silicon Valley defaults.
- Edge middleware eliminates complex proxy configurations for multi-tenant subdomains.
- Database RLS provides ironclad tenant boundaries with unified query performance.
Explore the live platform at resultapp.org.
Was this article valuable?
Leave a reaction below to let me know what you think.
Zabdiel Fwesh
zabdiel.tech →Full-stack software engineer & systems architect building resilient SaaS platforms, multi-tenant databases, and modern developer tooling.