School Management System - Multi-Tenant Laravel Platform
·Full-Stack Developer
A multi-tenant school-administration platform that lets a platform operator onboard schools and assign admins, while each School Admin manages their own teachers, grades, academic years, and student enrollments under role-based access control.
Small/mid schools juggle student records, teacher rosters, grade structures and yearly enrollments across spreadsheets with no shared platform that enforces school-level isolation when one operator runs multiple schools. This platform provides two administrative tiers (Super Admin for the operator, School Admin per school) so onboarding a new school is a guarded workflow rather than a manual DB edit, and a School Admin can never read or mutate another school's data — enforced at the model layer via Eloquent query scopes.
Role & Context
A Laravel 13 multi-tenant school-administration platform (Blade + Alpine + Tailwind 4). Two admin tiers: a Super Admin (platform operator) onboards schools and assigns admins, and a per-school School Admin manages teachers, grades, academic years, and student enrollments — with strict school-level data isolation as a non-negotiable constraint.
Problem
Small-to-mid schools typically juggle student records, teacher rosters, grade structures, and yearly enrollments across spreadsheets and ad-hoc tools, with no shared platform that enforces school-level data isolation when one operator runs multiple schools. The platform adds two administrative tiers so onboarding is a guarded workflow and one school admin cannot touch another school's data.
Approach & Architecture
Loading diagram…
Role middleware gates four audience-split route files; controllers re-derive school_id from the authenticated user and query through forSchool() scopes so cross-tenant access is impossible by construction.
Best Practices Followed
Strict multi-tenant isolation by construction: every tenant-scoped query goes through scopeForSchool($schoolId), so a forgotten WHERE in a controller can't leak cross-tenant rows
RBAC with defense in depth: RoleMiddleware guards the route, the controller re-derives school_id from the authenticated user (never from request input), and the UI hides per-role actions
Centralized API response shape: one ApiResponse helper (success/error/unauthorized/forbidden/notFound/validationError) keeps every endpoint consistent in payload and HTTP status
Form Request validation per endpoint: 19 dedicated FormRequest classes with rules() + messages() keep controllers thin and validation declarative
Reversible, ordered migrations with proper down(); soft deletes on Student/Teacher for audit trail; UUIDs auto-set in model boot() for User and School
Reproducible local setup: composer setup (install -> env -> key -> migrate -> npm build) and composer dev (server + queue + logs + Vite) so a contributor goes clone-to-running in one command
Challenges & Resolution
ChallengePreventing cross-tenant data leaks when one User model serves both Super Admin and School Admin against shared global tables (teachers, students) linked to schools via pivot tables.
FixEnforced isolation at the model layer with Eloquent query scopes (scopeForSchool, scopeActive) and resolved school_id from the authenticated user inside the controller rather than trusting any client-supplied id, so even a tampered form field can't reach another school's rows.
ChallengeManaging the academic-year lifecycle (pending -> active -> completed) safely so enrollments attach to the correct year and state transitions don't corrupt history.
FixModelled academic years and academic-grade-sections as first-class entities with explicit status, scoping enrollments to the active year per school.
Outcomes
Shipped: Auth + RBAC, Super Admin user management, school onboarding with admin assignment, teacher/student/grade/grade-section management, academic-year lifecycle, and student enrollment with transfer/completed/dropped states