Role is global-max, and the app is really binary
Two tacit facts about permissions, both dangerous.
1. The effective role is the max across all companies
useUserRole reduces highestRole over every company_memberships row the user has,
using a ROLE_PRIORITY map — not scoped to the active company:
const highestRole = memberships.reduce((best, m) =>
(ROLE_PRIORITY[m.role] ?? 0) > (ROLE_PRIORITY[best] ?? 0) ? m.role : best, ...);
A user who is super_admin in company A and employee in company B is treated as
super_admin everywhere. All the derived booleans (isSuperAdmin, isHrAdmin,
isDecider, …) ignore which tenant you are acting in. Because
the active company is in-memory only and RLS doesn't
enforce roles (client-side integrity), this is a real
privilege-bleed across tenants.
primaryCompanyId = companyIds[0] with no ORDER BY — the "primary" company is
whichever row Postgres returns first. Non-super-admins are hard-pinned to it
(effectiveCompanyId = isSuperAdmin ? activeCompanyId : primaryCompanyId, a ternary
copy-pasted across AdminLayout, Dashboard, Cases, Settings, OrgAuditLog), with
no company switcher — a multi-company regular user can only ever see their first company.
2. Despite a 12-value role enum, the UI is binary
Across the layout/nav/audit layer, isSuperAdmin is the only role signal used. There
are two nav configurations, not per-role menus, and isHrAdmin (a super-admin-inclusive
OR-gate) is reused as the single permission for archive, delete, audit visibility, and
user management. Splitting it in a rebuild changes all of them at once.
3. Two role vocabularies are written by different screens
Onboarding/CompanySetup invite users as company_admin / case_handler / viewer
(the legacy set), while UserManagement writes case_manager / hr_admin (the new set).
useUserRole aliases legacy→new, but the same conceptual role ends up stored as two
different strings depending on which admin surface created the user. Default invite role
even differs by screen (case_manager vs case_handler).
What breaks: a rebuild that models roles as clean, tenant-scoped RBAC will (a) close a privilege-bleed the PoC silently allowed — verify no workflow depends on it — and (b) orphan users created under the other vocabulary. Needs a role-normalisation migration.
Rebuild implication
Role must be evaluated per active tenant, server-side. Normalise the legacy/new role strings. Model the real role matrix (instructor / legal reviewer / decider / …) that the domain needs, not a super-admin boolean.