CampusCare
A campus platform where student and admin are two genuinely different trust levels, not two different themes — access is enforced at the API layer, where it actually matters.
Problem
A shared frontend has two categories of user — students and admins — who need to see and do genuinely different things. The easy version of this is a UI that hides an "Admin" button from students. The real version is a system where a student's request for admin-only data is rejected by the server itself, regardless of what the client sends or how the request is crafted — because a UI toggle is not a security boundary.
Constraints
- Every protected route needed a role check before its handler ran — not a check the handler remembered to do, one it couldn't skip.
- Admins needed read access to all student records; students needed access to only their own — the same data model had to support both without widening a student's query surface.
- One frontend, two functionally distinct dashboards — without duplicating the app per role.
Architecture
React frontend with role-aware routing · Node/Express REST API with role-checking middleware in front of every protected route · MongoDB for student and admin records · JWT carrying the authenticated role as a claim.
Key Engineering Decision
Role-checking middleware sits in front of every protected route, reading the role claim out of the verified JWT before the controller ever runs — so an admin-only endpoint rejects a student's token at the boundary, not somewhere inside application logic where a mistake could let it through. The React frontend reads the same claim to decide which dashboard shell to render, but that's UX convenience layered on top of a boundary that already holds without it.
Trade-offs
Authorization here is route-level, not resource-level — an admin route is either open to all admins or closed to all students, with no per-record scoping (e.g. a department-scoped admin who should only see their own department's students). That's the right amount of complexity for two roles; it would need a real permissions model before adding a third.
Lessons Learned
The natural order is to build the UI first and bolt permissions on after. Building the RBAC middleware first here reversed that — every route had to declare its required role before it existed, so there was never a retrofitting pass where a protected route accidentally shipped open. That ordering is the actual takeaway, more than the middleware code itself.