Role-based access control
Four roles, enforced at three layers.
The roles
Student, teacher, admin and management. A role is a property of the account, not a mode. There is no role switcher — a person needing two roles has two accounts, which is precisely why sessions rotate on a role switch.
Three layers of enforcement
LayerChecksExample
RouteChecks: Which roles may call this endpoint: A student calling a teacher endpoint is rejected before any handler runs.
RepositoryChecks: Which institution the caller belongs to: Every query is scoped by institution at the data boundary.
ResourceChecks: Ownership within the role: A teacher may edit their own course, not a colleague’s.
Hiding a control is not a permission
Every rule above is applied server-side. The student-side content-generation surface was removed and generation was made teacher-only on the server — removing the button alone would not have been a fix.
The guest lane
One unauthenticated surface exists: a public Open Quiz link. It is a separate lane with its own endpoints rather than an anonymous user inside the normal permission model, which keeps the blast radius of a leaked link to one quiz.