← MHD Andra Fahreza
--:-- UTC+7CV ↓
Case study · Multi-tenant SaaS · Education

Multi-tenant school management SaaS

One platform, many tenants, fully isolated data — 40+ schools on a single codebase. The same tenancy architecture now runs esko.id, a lighter attendance-only product spun out of it.

ROLE
Sole architect & engineer
SECTOR
Multi-tenant SaaS · Education
STATUS
Live in production

CONTEXT

A regional group of schools each needed a real operations platform — enrollment, attendance, grading, finance, and staff management. Building and hosting a separate application per school was never going to scale: every school would mean another deployment to patch, secure, and back up.

THE PROBLEM

Serve many schools from one codebase with data isolation strict enough that a bug can never leak one school's students to another — while each school configures its own roles and workflows.

CONSTRAINTS

  • Hard data isolation — a bug must never expose cross-tenant data.

  • Runs affordably on a single VPS footprint — not per-tenant infrastructure.

  • Real-time attendance and announcements without heavy infrastructure.

  • Onboarding a new school takes minutes — a record, not a deployment.

WHAT I DESIGNED

One Laravel application serving every school, with school access resolved through a single service and applied explicitly at each query. Attendance taps arrive from RFID devices over ADMS and are handed to a queue worker. Chat runs over Laravel Reverb for the Flutter client. Everything sits on one VPS.

Architecture diagram

Key technical decisions

Every one is a tradeoff. Here's what I chose and what it cost.

01

Explicit tenant scoping, not a global scope

Cross-school reads are a first-class case here, not an edge case — a parent with children at two schools, a supervisor role spanning a group of schools, a super-admin reviewing any of them. So one service resolves which schools a user may touch, and every query applies that filter explicitly and fails closed on an empty set.

Tradeoff: A global scope would enforce isolation for free, but I'd be fighting it on every legitimate multi-school view. The cost is that the filter is applied by hand and nothing catches a missed one — so the next step is a trait with a global scope plus an explicit opt-out for the cross-school reads.

02

Device taps are queued, never processed inline

The RFID machines speak ADMS and can't authenticate, so the endpoint stays thin: validate the serial, hand the tap to a queue worker, ack immediately. The device never waits on attendance rules, notifications, or WhatsApp.

Tradeoff: The attendance display is eventually consistent rather than instant, which is invisible on a wall screen and worth it for a device that would otherwise time out.

03

One VPS: database queues, Reverb for chat, polling for the display

No dedicated broker. Queues run on the database driver, Reverb serves chat on the same box, and the live attendance display just polls every five seconds — a wall monitor doesn't need a websocket.

Tradeoff: The database queue driver polls and won't scale forever. Redis is the obvious next step, and I'd move when throughput demands it rather than before.

INTERFACE

Admin Dashboard
Role Permission

Outcome

40+ schools run on one codebase with zero cross-tenant data incidents. A new school is onboarded in minutes, and every tenant is served from a single VPS footprint.

STACK

LaravelRole permissionsMySQLFlutterReverb

Have a system that needs building — or fixing?

Tell me the problem and the constraints. I'll tell you honestly whether I'm the right person and how I'd approach it.

Download CV ↓← See more work