Roles & features
How the OTHER role + per-user enabledFeatures combine with the existing role enum to gate the Tools Portal without disturbing the clinical apps.
The role enum
Role (defined in apps/api/prisma/schema.prisma):
enum Role {
PLATFORM_ADMIN // OHM staff, cross-org
GROUP_ADMIN // Hospital group
ORG_ADMIN // Hospital
DOCTOR
NURSE
PHARMACIST
RECEPTIONIST
LAB_TECH
BILLING
OTHER // Non-clinical bucket — features are per-user
}OTHER is intentionally a single bucket. We don't add new roles for
new non-clinical personas (medical coder, claims analyst, analytics
user, audit reviewer…). Instead we add a feature flag under the
existing OTHER role and assign it per user.
Why one generic role, not many specific ones? Adding a role
(MEDICAL_CODER, BILLING_ANALYST, …) means touching the role enum,
the invitation DTO, every relevant guard, and the admin UI dropdown
for each new persona. Adding a feature flag is just one new entry in
enabledFeatures and a checkbox in the admin UI. Cost difference
matters when shipping tools 2, 3, 4…
Three layers of authorization
A /api/codes/* request goes through three checks:
1. Authentication — JwtAuthGuard
Same as every other route. JWT must verify, user must exist + be active, org must be active + non-deleted.
2. Role allowlist — RolesGuard + @Roles(...)
The codes-tool controller declares its allowlist:
@Roles(
Role.DOCTOR, // can use codes inline in visit notes
Role.OTHER, // medical coders / billers
Role.ORG_ADMIN,
Role.GROUP_ADMIN,
Role.PLATFORM_ADMIN,
)Notably absent: NURSE, PHARMACIST, RECEPTIONIST, LAB_TECH,
BILLING. They keep their existing access to the shared
/api/codes/icd10/search-style endpoints (those have a wider
allowlist), but they don't get AI extract / favorites / lists.
3. Feature flag — @RequiresFeature('codes') + RequiresFeatureGuard
The guard checks both layers:
- Org-level:
Organization.features.codes === true. This is the paywall — sales / billing controls who has bought the tool. Default off. - Per-user:
User.enabledFeatures.includes('codes'). The org admin decides which specific users get the tool. Default empty[].
Either failing → 403 Forbidden with a clear message.
The guard reads enabledFeatures from req.user (populated fresh
from DB by the JWT strategy on every request) and falls back to a
direct DB query when the field is missing. This means changes from
the admin take effect on the user's next page load — no logout
required.
Global belt-and-braces — OtherRoleGuard
Registered as APP_GUARD in app.module.ts. Runs after JwtAuthGuard
on every authenticated request, regardless of which controller it hits.
Policy:
- If
user.rolesis exactly[OTHER], allow only paths in this list:/api/codes/*(the Codes Tool surface)/api/auth/*(login, logout, refresh)/api/users/me*(self-profile)
- Everything else for that user →
403. - For users with any other role (
DOCTOR, mixed admins, etc.), short-circuit to allow.
This is the safety net. The per-controller @Roles(...) decorators
already keep OTHER out of PHI controllers — but this guard is what
catches a future PHI route that someone forgets to decorate.
Adding a new tool (template)
When you ship tool number two — say, an Analytics Dashboard:
- Pick a flag name in
apps/api/src/codes-tool/...— e.g.analytics. - Add a new backend module with
@RequiresFeature('analytics')on every endpoint. - Allowlist the path in
OtherRoleGuard.ALLOWLIST(add/api/analytics/). - Add a UI catalogue entry in
apps/admin/src/pages/admin/InvitationsPage.tsx(the feature card list) andUsersManagement.tsx(theFEATURE_CATALOGconstant). - Add the sidebar item in
apps/tools/src/components/layout/AppShell.tsxwithfeature: "analytics".
No role changes. No invitation DTO changes. No global redirect changes. That's the win.
Who can edit what
| Role | Can invite role OTHER? | Can toggle features on existing user? | Can enable feature at org level? |
|---|---|---|---|
| Platform Admin | ✓ | ✓ | ✓ (SQL today — UI later) |
| Group Admin | ✓ | ✓ | ✗ |
| Org Admin | ✓ | ✓ | ✗ |
| Anyone else | ✗ | ✗ | ✗ |
The invitation and admin user endpoints are gated by @OrgAdmin(),
which includes Platform / Group / Org admins.
Onboarding a non-clinical user
End-to-end flow for getting a medical coder or billing staff member onto the Tools Portal — from DNS through their first AI extraction.
Codes Tool API
REST endpoints under /api/codes/* that power the Tools Portal's AI extraction, search, crosswalks, favourites, and lists.