
“Separation in the client is organisation, not security.”
Five kinds of user, one codebase
Service vendors, fleet carriers, owner operators, company drivers and a call centre — one product, five experiences, permissions resolved by type and role.
The platform served five kinds of user with genuinely different jobs. A vendor manages listings, invoices and renewals. A driver has recent searches, preferred vendors, call logs, trucks and repair tickets. A call centre operator works on behalf of others.
All of it from one codebase, with a separate internal CRM for the company's own team.
The problem
The constraint
if statements.What we did
Separated by route, not by condition
Each user type has its own set of routes under
/dashboard/{user_type}/{page}. To know what a vendor can do you read the vendor's routes — not the whole application looking forifstatements.One route set, one answerResolved permissions as type plus role
Every user type carries at least two roles — admin, owner, operator — and a permission resolves as the combination. A vendor's operator is a different subject from a fleet carrier's operator, even though both are called “operator”.
Same name, different subjectChecked on the backend as well
Route-level separation in the client is organisation, not security. The API validates independently, because anything enforced only in the interface is enforced only against people who use the interface.
Organisation is not securitySplit the API in two
Public routes and admin routes are separate sets rather than one set with checks inside. The internal CRM is effectively its own product against its own surface.
506 routes, two sets
Manages listings, invoices and renewals. Their operator sees listings but not invoices — the same role name, a different user type, a different permission.
The result
- Five user types and their roles from one codebase
- 506 API routes across two separate route sets
- Permissions auditable by reading one set of routes rather than the whole application
What we’d do differently
The permission model we built lived in the routing and in the API, but we never wrote it down anywhere a person could read as a whole. Onboarding a developer meant explaining it verbally, and verbal explanations of authorization models drift. A single generated table — user type against action — would have taken a day and answered the question permanently.
- React
- Redux
- Node.js
- Express
- MySQL