
Building a Multi-Tenant School Management SaaS Platform
Maktab One is a multi-tenant school management SaaS platform built for mid-level schools in Pakistan. It automates fee collection, expense tracking, student records, and guardian communication - replacing paper-based workflows that were error-prone and time-consuming.
The platform serves administrators, accountants, teachers, and parents through role-specific dashboards. Each school gets an isolated tenant with its own data, configuration, and user base, while sharing the same underlying infrastructure.
Before Maktab One, school administrators spent 10–15 hours per week manually tracking fees in ledgers, generating receipts, and reconciling payments. The platform eliminated manual tracking entirely, reduced administrative overhead by 80%, and gave school owners real-time visibility into their financial health.
Mid-level private schools in Pakistan operate on thin margins. Most cannot afford enterprise school management systems like SAP or Oracle - let alone the IT staff to maintain them. At the same time, free alternatives lack the features these schools actually need: local currency support, Urdu-language receipts, SMS notifications, and offline-capable workflows.
The existing workflow was entirely paper-based. Accountants maintained physical ledgers for fee records, issued handwritten receipts, and reconciled payments manually at the end of each month. Errors were common: missed payments, incorrect balances, lost receipts. When parents disputed a payment, resolving it meant digging through months of paper records.
School owners had no real-time visibility into their finances. They could not see which students had outstanding fees, how much revenue was expected this month, or where expenses were trending - without manually compiling reports that took hours to produce.
The key constraint was cost. Schools needed a solution priced at a fraction of enterprise alternatives, deployable without dedicated IT staff, and accessible from any device with a browser.
Business Goals
- Create a recurring revenue stream through monthly SaaS subscriptions per school
- Reduce schools' administrative overhead by at least 70%
- Enable school owners to make data-driven financial decisions in real time
- Keep the price point accessible for mid-tier private schools
Technical Goals
- Build a multi-tenant architecture with strong data isolation between schools
- Achieve sub-second page loads for core workflows (fee tracking, student search)
- Support concurrent access from 50+ users per school during peak hours
- Ensure 99.5% uptime with automated backups and disaster recovery
User Goals
- Administrators need a complete financial overview with drill-down to individual students
- Accountants need to record payments, generate receipts, and reconcile dues in under 30 seconds per student
- Teachers need to view attendance-linked fee data for their classes
- Parents need to view fee status and download receipts without contacting the school
Functional
Non‑Functional
Performance
- • Page load times under 1.5 seconds on standard broadband connections
- • Database queries for fee summaries complete in under 200ms
- • PDF receipt generation completes in under 2 seconds
- • Support for up to 2,000 students per tenant without degradation
Security
- • Row-level security ensuring Tenant A cannot access Tenant B's data
- • Password hashing using bcrypt with cost factor 12
- • JWT-based authentication with refresh token rotation
- • Rate limiting on login endpoints to prevent brute force attacks
- • Input sanitization on all user-facing fields
- • HTTPS enforcement and secure cookie configuration
Scalability
- • Horizontal scaling of API server through stateless architecture
- • Database connection pooling for efficient resource utilization
- • Read replicas for reporting queries that scan large datasets
- • Tenant-aware caching to reduce database load for repeated queries
Maintainability
- • TypeScript throughout for type safety and developer experience
- • Prisma ORM for database migrations and schema versioning
- • Comprehensive API documentation using OpenAPI spec
- • Modular folder structure separating concerns by domain
- • CI/CD pipeline with automated testing before deployment
System Flow
- 1Users access the application through a web browser. The Next.js frontend handles initial page load via server-side rendering for SEO and performance, then transitions to client-side navigation for subsequent page views.
- 2API requests are proxied through Next.js API routes or go directly to the Express.js backend for non-page resources. The Express API authenticates requests using JWT tokens, validates input, and routes to the appropriate service layer.
- 3The service layer contains business logic organized by domain: student management, fee processing, expense tracking, and reporting. Each service interacts with the database through Prisma, which provides type-safe queries and handles connection pooling.
- 4Fee processing follows a multi-step workflow: recording a payment triggers receipt generation, updates the student's fee ledger, logs the transaction, and sends a notification to the guardian. This is implemented as a transactional unit to ensure data consistency.
- 5Background jobs handle email notifications (via Resend), SMS alerts, and automated fee reminder generation using a simple job queue pattern within the Express.js process.
Database Design
- The database uses a shared schema with tenant isolation through a `tenantId` column on every tenant-scoped table. This approach balances the operational simplicity of a single database with strong programmatic isolation.
- The `School` (tenant) table stores configuration per school: branding settings, fee structures, academic calendar, and subscription status. Every student, fee record, expense, and user account references a `schoolId`.
- The `Student` table tracks demographic info, enrollment status, class assignments, and guardian details. Fee records are stored in a `FeeLedger` table with a normalized schema: each row represents a single fee item for a student in a given month, with fields for amount, due date, paid date, and status.
- Indexes are strategically placed on `(schoolId, status)`, `(schoolId, dueDate)`, and `(studentId, dueDate)` to accelerate the most common query patterns: fee summaries by status, overdue fee lookups, and individual student fee history.
- Prisma migrations manage schema evolution. Each migration is reviewed for backward compatibility before applying to production. Rollback scripts are maintained for critical migrations.
Request Flow
- 1A guardian opens the fee portal and logs in. The Next.js frontend sends credentials to `/api/auth/login`. The Express server validates credentials against the database, generates a short-lived access token (15 minutes) and a long-lived refresh token (7 days).
- 2The frontend stores the access token in memory and the refresh token in an HTTP-only cookie. On page load, the server renders the initial state using the refresh token to authenticate.
- 3When the guardian views the fee dashboard, the frontend calls `/api/fees/student/:id`. The Express middleware extracts the tenant context from the JWT, verifies the requesting user has access to this student's data, and queries the FeeLedger table filtered by both `schoolId` and `studentId`.
- 4The response includes the student's fee summary (total due, paid, overdue), a list of recent transactions, and upcoming dues. The frontend caches this response using TanStack Query with a 30-second stale time.
- 5When recording a payment, the accountant submits a POST to `/api/fees/pay`. The server validates the payment amount against the outstanding balance, creates a transaction record, generates a PDF receipt, sends a notification, and returns the updated fee summary - all within a database transaction.
Deployment Flow
- 1The Express API, Next.js frontend, and PostgreSQL database run as Docker containers on a single VPS with 2 vCPUs and 4GB RAM. Docker Compose orchestrates the services.
- 2Nginx serves as the reverse proxy, handling SSL termination with Let's Encrypt certificates and forwarding requests to the appropriate container based on the route.
- 3Database backups run automatically every 6 hours using pg_dump, with backups stored both on the server and synced to an S3-compatible object store. Point-in-time recovery is supported through WAL archiving.
- 4The CI/CD pipeline (GitHub Actions) runs linting, type checking, and integration tests on every push to main. A successful pipeline triggers a deployment via SSH: pulling the latest Docker images, running database migrations, and restarting containers with zero-downtime through rolling updates.
- 5Vercel serves as a CDN for static assets (images, fonts), reducing load on the application server and improving page load times for users across different geographic regions.
Architectural Decisions
Shared schema with tenantId vs. separate databases per tenant
We chose a shared schema with row-level tenant isolation over separate databases per tenant. This decision was driven by operational simplicity: a single database is easier to manage, backup, and migrate. The trade-off is that a query without a tenantId filter could leak data across tenants. We mitigated this by making tenantId required in every service function and adding automated tests that verify tenant isolation at the integration level.
Prisma ORM vs. raw SQL or Drizzle
Prisma was chosen for its type-safe query builder, automated migration generation, and excellent developer experience. The Prisma schema serves as a single source of truth for the database structure. The trade-off is that Prisma adds latency for complex join queries compared to raw SQL. We mitigated this by using raw queries for the few performance-critical reports and Prisma for the remaining 95% of queries.
Monolithic Express API vs. microservices
A monolithic Express.js API was the pragmatic choice for the current scale. The application has a bounded domain (school management) with tight coupling between features - fees depend on students, expenses depend on fee collection, etc. Microservices would add network overhead, data consistency challenges, and deployment complexity without proportional benefit at this stage. The codebase is structured in domain modules, making it straightforward to split into services if scale demands it.
TanStack Query vs. Redux or Zustand
TanStack Query was chosen over state management libraries because the application's state is primarily server-derived. TanStack Query handles caching, background refetching, optimistic updates, and error handling with minimal boilerplate. This eliminated an entire class of bugs related to stale data and race conditions that would be common with a manual state management approach.
PDF generation at the API layer vs. client-side
PDF receipts are generated server-side using a headless approach rather than in the browser. Server-side generation ensures consistent output regardless of the client device or browser, supports batch generation for bulk operations, and produces smaller file sizes. The trade-off is increased server load during receipt generation, but this is acceptable given the infrequent nature of individual receipt generation.
Next.js provides server-side rendering for initial page loads (critical for SEO and perceived performance), API routes for lightweight backend endpoints, and file-based routing that keeps the codebase organized. Its support for React Server Components allows fee data to be fetched at the server level, reducing client-side JavaScript bundle size.
Trade-off
Express.js was chosen for the API layer because of its ecosystem maturity, middleware pattern that maps cleanly to our authentication and validation needs, and minimal overhead for REST endpoint definition. The Node.js event loop handles concurrent requests efficiently without the threading complexity of alternatives like Django or Spring Boot.
Trade-off
PostgreSQL was selected for its robust support for concurrent transactions (essential for fee processing), JSONB columns for storing flexible configuration data per tenant, and powerful indexing capabilities that keep query performance predictable under load. Its ecosystem includes excellent tooling for backup, replication, and monitoring.
Trade-off
Prisma's type-safe query builder eliminates an entire category of runtime errors from malformed queries. Its migration system makes schema evolution reviewable in pull requests. The Prisma Studio provides a visual interface for ad-hoc data exploration during development and debugging.
Trade-off
Docker ensures consistency across development, staging, and production environments. Docker Compose allows the entire stack (API, database, reverse proxy) to run locally with a single command, reducing onboarding time for new developers. Containers simplify the CI/CD pipeline by providing a reproducible build artifact.
Trade-off
Vercel provides global CDN distribution for static assets, automatic SSL certificate management, and seamless integration with the Next.js framework. Its edge functions handle authentication token verification at the network edge, reducing latency for API calls.
Trade-off
Multi-Tenant Fee Management
Problem
Schools needed to configure unique fee structures - different amounts per grade, optional fees for transport/lunch, late payment penalties, and scholarship discounts. Existing solutions either forced a rigid structure or required custom development per school.
Solution
We built a configurable fee engine that allows administrators to define fee slabs by class and category. Each slab supports base amount, optional add-ons, discount rules, and due date scheduling. The engine auto-calculates totals, applies proration for mid-term admissions, and generates payment schedules for the entire academic year.
Engineering Challenges
The most complex challenge was handling mid-term adjustments: a student joining in November should only be billed for remaining months, with transport fees prorated differently than tuition. We implemented a date-range-aware calculation system where each fee slab specifies its applicable period, and the engine computes proportional amounts based on the student's enrollment window within that period.
Role-Based Access Control
Problem
A school management system serves four distinct user types: administrators who need full access, accountants focused on financial data, teachers who view attendance-linked information, and parents who should only see their own children's records. Each role has different data access needs within and across modules.
Solution
We implemented a hierarchical RBAC system with 12 granular permissions mapped to user roles. Permissions follow a {module}:{action} pattern (e.g., fees:create, students:read). Roles are configurable per school, allowing each tenant to customize access for their staff structure. The middleware layer evaluates permissions on every API request, returning 403 immediately if the user lacks access.
Engineering Challenges
The main challenge was balancing granularity with usability. Too many permissions made role configuration confusing for school administrators. We compromised by defining 4 default roles with sensible defaults (admin, accountant, teacher, guardian) while allowing super-admins to create custom roles with specific permission combinations.
Automated Notification System
Problem
Schools needed to communicate fee due dates, overdue reminders, and payment confirmations to parents. Manual communication via phone calls or paper notices was inconsistent and time-consuming. Parents expected digital notifications but many did not use smartphone apps.
Solution
We built a multi-channel notification system that sends fee reminders via email (through Resend) and SMS (through a local SMS gateway provider) based on configurable triggers: 7 days before due date, on the due date, and 3/7/14 days after missed payment. Payment confirmations are sent immediately after recording a payment, including a link to download the receipt.
Engineering Challenges
SMS delivery in Pakistan is unreliable - messages can be delayed by hours or silently dropped. We implemented a delivery tracking system that marks notifications as sent/pending/failed, with automatic retry after 30 minutes for failed SMS deliveries. Email serves as the reliable fallback channel. We also added a notification preference system so guardians can choose their preferred channel.
Tenant data isolation while maintaining a shared schema
We implemented a middleware layer that injects the tenant context into every request automatically. All database queries include a mandatory `schoolId` filter that is validated by Prisma's middleware hook. We added integration tests that verify tenant isolation: creating data as Tenant A and asserting Tenant B cannot access it. This catches isolation violations at the CI level before they reach production.
Trade-off
PDF receipt generation at scale
Receipt generation was initially a synchronous operation that blocked the API response for 2-3 seconds. For batch operations (generating 50 receipts at end-of-month), this caused request timeouts. We moved receipt generation to a background job queue with the following architecture: the API immediately returns a 202 response with a job ID, a worker process generates the PDF asynchronously, and the frontend polls for completion. Receipts are cached on disk after first generation.
Trade-off
Database query performance for aggregated fee reports
School owners frequently request reports like total outstanding fees per class or monthly collection trends. These aggregate queries scan thousands of fee ledger rows and were taking 3-5 seconds to complete. We introduced a materialized view that pre-computes daily snapshots of fee summaries by class, grade, and status. The reporting API queries this materialized view instead of scanning the raw ledger. The view refreshes every 15 minutes via a cron job, which is acceptable for reporting use cases.
Trade-off
- ▸Database indexes on (schoolId, status), (schoolId, dueDate), and (schoolId, studentId) reduced fee summary query times from 800ms to under 50ms.
- ▸TanStack Query's stale-while-revalidate caching strategy ensures the fee dashboard feels instant on subsequent visits. Data is shown from cache immediately while a background refetch updates it.
- ▸Next.js Image component automatically serves WebP images with responsive sizes, reducing image payload by 60% compared to PNG equivalents.
- ▸The Express API uses compression middleware to reduce JSON response sizes by 70-80%, particularly important for the fee ledger endpoint that returns hundreds of records.
- ▸React Server Components render the fee dashboard data on the server, eliminating a client-side data fetch roundtrip on initial page load. Subsequent navigations use TanStack Query for caching.
- ▸The PDF worker runs with a lower Node.js process priority to prevent receipt generation from starving API request handling of CPU resources.
- ▸Authentication uses a dual-token strategy: short-lived access tokens (15 minutes) stored in memory, and HTTP-only secure cookies for refresh tokens (7 days). This limits the blast radius of XSS attacks - stolen access tokens expire quickly.
- ▸All API responses include security headers: Content-Security-Policy restricts script sources, X-Content-Type-Options prevents MIME sniffing, and Strict-Transport-Security enforces HTTPS.
- ▸Rate limiting is configured per-IP and per-tenant on login, registration, and payment endpoints using an in-memory sliding window counter. Excessive requests trigger a 429 response with a Retry-After header.
- ▸Database queries use parameterized statements exclusively through Prisma, preventing SQL injection at the ORM level. User input is validated against defined schemas using Zod before reaching business logic.
- ▸Fee processing endpoints verify that the authenticated user belongs to the same tenant as the student being modified. This cross-tenant access check is enforced in a centralized authorization middleware.
- ▸Sensitive configuration (database URLs, API keys, JWT secrets) is stored as environment variables and injected at deployment time. Secrets are never committed to version control.
- 1Tenant isolation should be tested at the integration level, not just unit level. Our initial test suite only verified that middleware added the tenantId filter, but did not verify that a cross-tenant query actually returned empty results. Adding integration tests with two tenants caught two isolation bugs that unit tests missed.
- 2Prisma migrations in a multi-tenant schema require careful planning. Adding a NOT NULL column to a table with existing data requires a multi-step migration: add the column as nullable, backfill data, then add the NOT NULL constraint. We learned this the hard way when a migration failed in staging.
- 3PDF generation is deceptively complex. Libraries like Puppeteer require significant memory and CPU resources. For future projects, I would evaluate Wasmer-based PDF generation or a dedicated PDF microservice to avoid impacting API performance.
- 4School administrators have diverse technical skills. The simplest UI won and adoption was highest for features that mimicked their existing paper workflows (e.g., a ledger view that looks like a physical fee register). Over-engineering the UX created confusion - the right approach was to digitize their mental model, not replace it.
- 5Background job queues are worth implementing earlier than you think. What started as a synchronous receipt generation quickly became a performance bottleneck. A simple Bull queue with Redis would have saved a refactor sprint.
Roadmap
- 1Migrate to a dedicated job queue system (BullMQ with Redis) for all background operations: PDF generation, email delivery, SMS sending, and report generation. The current in-process approach does not scale beyond a single server.
- 2Add real-time dashboards using WebSocket connections for live fee collection tracking. School owners want to see today's collections update in real time during peak fee collection days.
- 3Implement an offline-capable Progressive Web App (PWA) mode for accountants who work in areas with unreliable internet. Local-first data sync using IndexedDB with conflict resolution.
- 4Introduce read replicas for reporting queries. The materialized view approach works but adds operational complexity. Dedicated read replicas would allow real-time reporting without impacting transactional performance.
- 5Build a public API for third-party integrations: accounting software (QuickBooks, Xero), payment gateways (Stripe, JazzCash), and communication tools (WhatsApp Business API).
- 6Add automated data migration tools for schools transitioning from existing systems (Excel sheets, other school management software). Import wizards with field mapping and validation.
Want to discuss this project?
I am always happy to talk about engineering decisions, architecture, or how I can help with your next project.

