OHMOHM Studio

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.

View as Markdown

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.roles is 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:

  1. Pick a flag name in apps/api/src/codes-tool/... — e.g. analytics.
  2. Add a new backend module with @RequiresFeature('analytics') on every endpoint.
  3. Allowlist the path in OtherRoleGuard.ALLOWLIST (add /api/analytics/).
  4. Add a UI catalogue entry in apps/admin/src/pages/admin/InvitationsPage.tsx (the feature card list) and UsersManagement.tsx (the FEATURE_CATALOG constant).
  5. Add the sidebar item in apps/tools/src/components/layout/AppShell.tsx with feature: "analytics".

No role changes. No invitation DTO changes. No global redirect changes. That's the win.

Who can edit what

RoleCan 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.