Currently

RBAC & permissions

RBAC & permissions

Last updated 5/2/2026

Currently Healthcare Data Platform — RBAC & Permissions Documentation

Document ID: TECH-SEC-001
PRD Lifecycle Stage: v0.9 Scale
Last Updated: Derived from repository state at commit 02461e4
Scope: Next.js web application (web/), Clerk authentication, API routes, CLI client


Table of Contents

  1. Role Hierarchy
  2. Permission Matrix
  3. Clerk Integration Architecture
  4. Protected Route Inventory
  5. Middleware Configuration
  6. Server-Side Auth Patterns
  7. Client-Side Auth Guards
  8. Row-Level Security Patterns
  9. API Key Scopes
  10. Audit Logging
  11. Business Rules — Access Control

1. Role Hierarchy

Currently operates four distinct application scopes, each mapped to a shell route and a set of Clerk organization roles. Roles are hierarchical within each scope: higher roles inherit all permissions of lower roles.

1.1 Scope-to-Route Mapping

ScopeRoute PrefixClerk Org ContextDescription
Platform Admin/adminPlatform-level orgCurrently employees; full system control
HIE Network/dashboard/networkHIE organizationHealth Information Exchange operators
Provider/Facility/dashboardFacility organizationHospital and clinic staff
Patient Portal/portalPer-patient identityPatients accessing their own records

1.2 Role Definitions

PLATFORM_ADMIN
    └── org:admin (platform org)
        ├── Full CRUD on all organizations, hospitals, users
        ├── Revenue and pricing management
        ├── API key governance across all orgs
        ├── PHI audit log access
        ├── Provider approval authority
        └── ATS (applicant tracking) access

HIE_ADMIN
    └── org:admin (hie org)
        ├── Manage facilities within HIE network
        ├── Approve/reject facility-to-HIE link requests
        ├── View all records within HIE scope
        ├── Manage HIE-scoped API keys
        ├── Network analytics and export
        └── User management within org

HIE_MEMBER
    └── org:member (hie org)
        ├── View network dashboard, patients, providers, records
        ├── Initiate file uploads
        ├── View processing jobs
        └── Read-only settings access

FACILITY_ADMIN
    └── org:admin (facility org)
        ├── Manage facility settings
        ├── Invite facility users
        ├── Link facility to HIE
        ├── View facility-scoped medical records
        └── Manage facility API keys

FACILITY_MEMBER
    └── org:member (facility org)
        ├── Upload clinical files (HL7, CDA, FHIR)
        ├── View own uploads and processing status
        └── View medical records (read-only)

PATIENT
    └── Individual Clerk user (no org required)
        ├── View own medical records
        ├── Manage sharing preferences
        ├── Generate personal API access tokens
        └── Update profile

UNAUTHENTICATED
    └── No Clerk session
        ├── Access public auth pages
        ├── Submit registration forms (facility, HIE)
        └── Accept invitation tokens

1.3 Database Role Storage

The users table in the Platform Cloud SQL instance persists two role fields:

-- web/src/db/schema (inferred from database schema)
clerk_role TEXT,          -- Raw role string synced from Clerk organization membership
role       TEXT NOT NULL DEFAULT 'member'  -- Normalized app role

Role synchronization occurs via the /api/auth/sync-user webhook endpoint, which receives Clerk organization membership events and updates both fields. See BR-001.


2. Permission Matrix

2.1 Core Resource × Action Matrix

ResourceActionplatform_adminhie_adminhie_memberfacility_adminfacility_memberpatient
Organizationscreate
read (all)
read (own)
update
delete
approve
Hospitals / Facilitiescreate
read (all)
read (own org)
update
link to HIE
Usersinvite (facility)
invite (patient)
list (org)
deactivate
Medical Recordsread (any org)
read (own facility)
read (own patient)
write / upload
export
Filesupload
read metadata
API Keyscreate (platform)
create (hie-scoped)
create (patient)
read metrics
revoke✅ (own)✅ (own)
Pricing / Revenueread
update tiers
Subscriptionscheckout
cancel
PHI Audit Logread
Analyticsplatform-wide
hie-scoped
Registrationssubmit (hie)public
submit (facility)public
review / approve
Provider Approvalapprove
ATSread
Sessionread own

3. Clerk Integration Architecture

3.1 Clerk Package Versions

// package.json (root devDependencies)
"@clerk/backend": "^2.29.5"

The web application (web/) consumes @clerk/nextjs for the Next.js App Router integration. The @clerk/backend package at the root is used by the CLI (cli/src/auth/resolve.ts, cli/src/auth/store.ts) for token verification.

3.2 Organization Role Mapping

Clerk Organizations directly model the multi-tenant structure. Each HIE and each facility is a distinct Clerk Organization. Currently maps Clerk's built-in org:admin and org:member roles to application roles as follows:

Clerk RoleClerk Organization TypeCurrently App Role
org:adminPlatform org (Currently internal)platform_admin
org:memberPlatform orgplatform_member (not currently used)
org:adminHIE organizationhie_admin
org:memberHIE organizationhie_member
org:adminFacility organizationfacility_admin
org:memberFacility organizationfacility_member
N/A (individual user)Nonepatient

The organizations.clerk_org_id column provides the join between Clerk's organization ID and the platform's internal organizations record. See BR-002.

3.3 Session Claims and Custom Metadata

Clerk session tokens carry publicMetadata and organizationMetadata that the application reads server-side. The sync-user endpoint (web/src/app/api/auth/sync-user/route.ts) writes the following fields to the users table upon login or membership change:

users.clerk_id        ← session.userId
users.email           ← session.primaryEmailAddress
users.name            ← session.fullName
users.clerk_role      ← active organization membership role
users.organization_id ← lookup via organizations.clerk_org_id
users.image_url       ← session.imageUrl

The patient scope requires no organization. A user with a valid Clerk session but no active organization membership is treated as a patient role when accessing /portal routes.

3.4 Webhook Integration

Clerk webhooks deliver real-time membership events to /api/auth/sync-user. This keeps the Platform Cloud SQL users table synchronized without polling. See BR-003.


4. Protected Route Inventory

4.1 Admin Routes — platform_admin Required

All routes under /api/admin/ and /api/v1/admin/ require the caller to hold the platform_admin role (Clerk org:admin within the Currently platform organization).

RouteFileMethodRequired RoleNotes
/api/admin/facility-orgs/createadmin/facility-orgs/create/route.tsPOSTplatform_adminCreates a facility organization record and provisions Clerk org
/api/admin/invitations/facility-useradmin/invitations/facility-user/route.tsPOSTplatform_adminSends facility user invitation email
/api/admin/invitations/patientadmin/invitations/patient/route.tsPOSTplatform_adminSends patient portal invitation
/api/admin/org-links/approveadmin/org-links/approve/route.tsPOSTplatform_adminApproves a facility-to-HIE linkage request
/api/admin/pricing/tiersadmin/pricing/tiers/route.tsGET, POST, PUTplatform_adminManages subscription pricing tiers
/api/admin/registrations/facilityadmin/registrations/facility/route.tsGETplatform_adminLists pending facility registrations
/api/admin/registrations/facility/:idadmin/registrations/facility/[id]/route.tsGET, PATCHplatform_adminReviews and approves a specific facility registration
/api/admin/registrations/facility/hie-initiateadmin/registrations/facility/hie-initiate/route.tsPOSTplatform_adminInitiates HIE-side facility onboarding
/api/admin/registrations/hieadmin/registrations/hie/route.tsGETplatform_adminLists pending HIE registrations
/api/admin/registrations/hie/:idadmin/registrations/hie/[id]/route.tsGET, PATCHplatform_adminReviews and approves a specific HIE registration
/api/admin/revenue/statsadmin/revenue/stats/route.tsGETplatform_adminPlatform-wide revenue dashboard data
/api/v1/admin/analytics/by-orgv1/admin/analytics/by-org/route.tsGETplatform_adminCross-org analytics aggregation
/api/v1/admin/api-keysv1/admin/api-keys/route.tsGET, POST, DELETEplatform_adminPlatform-level API key management
/api/v1/admin/api-keys/metrics/platformv1/admin/api-keys/metrics/platform/route.tsGETplatform_adminAggregate API key usage metrics
/api/v1/admin/approve-providerv1/admin/approve-provider/route.tsPOSTplatform_adminApproves a provider account
/api/v1/admin/audit/phi-accessv1/admin/audit/phi-access/route.tsGETplatform_adminPHI access audit log viewer

4.2 HIE Routes — hie_admin or hie_member Required

Routes under the HIE network dashboard scope require membership in a valid HIE organization.

RouteFileMethodRequired RoleNotes
/api/hieshies/route.tsGEThie_admin, platform_adminLists all HIE organizations
/api/hies/:idhies/[id]/route.tsGET, PATCHhie_admin, platform_adminGets or updates a specific HIE
/api/orgs/hieorgs/hie/route.tsGEThie_admin, hie_memberReturns the caller's HIE organization
/api/facilitiesfacilities/route.tsGET, POSThie_admin, platform_adminLists or creates facilities within an HIE
/api/facilities/:idfacilities/[id]/route.tsGET, PATCHhie_admin, hie_member, platform_adminFacility detail
/api/facilities/:id/hospitalfacilities/[id]/hospital/route.tsGEThie_admin, hie_member, facility_admin, facility_memberHospital record for facility
/api/facilities/:id/medical-recordsfacilities/[id]/medical-records/route.tsGEThie_admin, hie_member, platform_adminMedical records scoped to facility
/api/facility/link-to-hiefacility/link-to-hie/route.tsPOSTfacility_admin, platform_adminRequests facility-to-HIE linkage
/api/hospital/by-facility-orghospital/by-facility-org/route.tsGETfacility_admin, facility_member, hie_admin, hie_memberResolves hospital from org context
/api/filesfiles/route.tsGET, POSThie_admin, hie_member, facility_admin, facility_memberFile registry access
/api/subscriptions/checkoutsubscriptions/checkout/route.tsPOSThie_admin, facility_adminInitiates Stripe checkout
/api/subscriptions/cancelsubscriptions/cancel/route.tsPOSThie_admin, facility_adminCancels active subscription

4.3 Patient Routes — patient (authenticated individual) Required

RouteFileMethodRequired RoleNotes
/api/onboarding/patientonboarding/patient/route.tsPOSTAuthenticated userPatient onboarding form submission
/api/sessionsession/route.tsGETAny authenticated userReturns current session metadata

4.4 Shared Auth Routes

RouteFileMethodAuth RequiredNotes
/api/auth/sync-userauth/sync-user/route.tsPOSTClerk webhook secretClerk webhook; verified via svix signature
/api/invitations/:tokeninvitations/[token]/route.tsGETNoneToken-based invitation lookup (unauthenticated)
/api/invitationsinvitations/route.tsPOSTfacility_admin, platform_adminSends invitation; role checked server-side
/api/npi/verifynpi/verify/route.tsGETAny authenticated userNPI registry lookup; requires login but no specific role
/api/organizations/approvedorganizations/approved/route.tsGETAny authenticated userReturns approved orgs for dropdowns
/api/organizations/check-hieorganizations/check-hie/route.tsGETAny authenticated userChecks HIE membership status
/api/registrations/hieregistrations/hie/route.tsPOSTNone (public)Public HIE self-registration form
/api/registrations/facilityregistrations/facility/route.tsPOSTNone (public)Public facility self-registration form

4.5 Public / Health Routes

RouteFileMethodAuth RequiredNotes
/api/healthhealth/route.tsGETNoneLiveness check; no auth
/api/dev/email-previewdev/email-preview/route.tsGETDevelopment env onlyPreview email templates; blocked in production

4.6 UI Page Route Protection

Page RouteShellRequired RoleClerk Component
/admin/*Platform Adminplatform_adminauth() server-side + middleware redirect
/admin/analyticsPlatform Adminplatform_adminServer component auth check
/admin/ats/*Platform Adminplatform_adminServer component auth check
/admin/api-docsPlatform Adminplatform_adminplatform-admin-shell.tsx gate
/dashboard/network/*HIE Networkhie_admin, hie_memberMiddleware + layout auth check
/dashboard/network/settings/api-keysHIE Networkhie_adminAdmin-only UI gate
/dashboard/network/settings/modulesHIE Networkhie_adminAdmin-only UI gate
/dashboard/network/approvalsHIE Networkhie_adminAdmin-only UI gate
/dashboard/network/exportHIE Networkhie_adminAdmin-only UI gate
/portal/*PatientAuthenticated (no org)portal/layout.tsx auth check
/portal/settings/api-accessPatientAuthenticatedpatient-api-access-client.tsx guard

5. Middleware Configuration

The Next.js middleware file (web/src/middleware.ts) is the first line of defense for all route protection. Based on the four-shell architecture and Clerk integration:

5.1 Inferred Middleware Behavior

// web/src/middleware.ts (inferred structure)
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import { NextResponse } from 'next/server';

// Public routes that bypass auth entirely
const isPublicRoute = createRouteMatcher([
  '/',
  '/sign-in(.*)',
  '/sign-up(.*)',
  '/register/(.*)',          // HIE and facility self-registration
  '/api/registrations/(.*)', // Public registration form submissions
  '/api/invitations/(.*)',   // Invitation token acceptance
  '/api/health',             // Liveness probe
  '/accept-invite(.*)',
]);

// Admin-only shell
const isAdminRoute = createRouteMatcher(['/admin(.*)']);

// HIE-scoped shell
const isHIERoute = createRouteMatcher(['/dashboard/network(.*)']);

// Patient portal
const isPatientRoute = createRouteMatcher(['/portal(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (!isPublicRoute(req)) {
    await auth.protect();  // Redirect to /sign-in if no session
  }

  // Admin routes: verify platform org membership
  if (isAdminRoute(req)) {
    const { orgRole, orgSlug } = await auth();
    if (orgRole !== 'org:admin' || orgSlug !== process.env.PLATFORM_ORG_SLUG) {
      return NextResponse.redirect(new URL('/dashboard', req.url));
    }
  }
});

export const config = {
  matcher: ['/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', '/(api|trpc)(.*)'],
};

See BR-004 and BR-005 for the business rules governing middleware redirect behavior.

5.2 Clerk Branding

Commit b68c500 ("fix: show Currently branding on auth pages") indicates that Clerk's hosted sign-in/sign-up pages are customized with Currently branding. The NEXT_PUBLIC_CLERK_* environment variables control the appearance.


6. Server-Side Auth Patterns

6.1 auth() Usage in API Routes

All protected API routes use Clerk's auth() helper from @clerk/nextjs/server. The standard pattern across admin routes:

// Pattern: admin route auth check
// e.g., web/src/app/api/admin/registrations/hie/route.ts
import { auth } from '@clerk/nextjs/server';

export async function GET() {
  const { userId, orgRole, orgId } = await auth();

  if (!userId) {
    return new Response('Unauthorized', { status: 401 });
  }

  if (orgRole !== 'org:admin' || orgId !== process.env.PLATFORM_CLERK_ORG_ID) {
    return new Response('Forbidden', { status: 403 });
  }

  // ... handler logic
}

6.2 currentUser() Usage

The sync-user endpoint and onboarding routes use currentUser() to access full user profile data including email, name, and image URL when writing to the users table:

// Pattern: user sync
// web/src/app/api/auth/sync-user/route.ts
import { currentUser } from '@clerk/nextjs/server';

export async function POST() {
  const user = await currentUser();
  if (!user) return new Response('Unauthorized', { status: 401 });

  // Upsert into users table using user.id (clerk_id), user.emailAddresses, etc.
}

6.3 Organization-Scoped Checks

Routes that scope data to the caller's organization extract orgId from the auth object and join it against organizations.clerk_org_id:

// Pattern: org-scoped data access
// e.g., web/src/app/api/facilities/route.ts
const { userId, orgId } = await auth();

const org = await db.query.organizations.findFirst({
  where: eq(organizations.clerk_org_id, orgId)
});

// All subsequent queries filter by org.id

See BR-006 for the org-scoping rule.

6.4 Webhook Signature Verification

The /api/auth/sync-user endpoint is not protected by Clerk session auth. Instead it verifies the svix-id, svix-timestamp, and svix-signature headers using the Clerk webhook secret:

// Pattern: webhook verification
import { Webhook } from 'svix';

const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET!);
const evt = wh.verify(body, headers); // Throws on invalid signature

See BR-007.


7. Client-Side Auth Guards

7.1 SignedIn / SignedOut Components

Auth pages use Clerk's React components to conditionally render content. The b68c500 commit applied Currently branding to Clerk's hosted UI, meaning sign-in/sign-up flow is managed by Clerk's hosted pages rather than custom React components.

7.2 useAuth Hook — Role-Conditional UI

The HIE network shell (web/src/app/(app)/(hie)/dashboard/network/layout.tsx) and platform admin shell (platform-admin-shell.tsx) use useAuth() to conditionally render admin-only navigation items:

// Pattern: conditional nav rendering
// web/src/app/(app)/(hie)/dashboard/network/settings/api-keys/hie-api-keys-page-client.tsx
import { useAuth } from '@clerk/nextjs';

export function HieApiKeysPageClient() {
  const { orgRole } = useAuth();

  if (orgRole !== 'org:admin') {
    return <AccessDenied />;
  }

  return <ApiKeysManagement />;
}

7.3 useUser Hook — Patient Identity

The patient portal (portal/layout.tsx) uses useUser() to extract the user's identity for record filtering:

// Pattern: patient identity in portal
import { useUser } from '@clerk/nextjs';

export function PatientPortalLayout() {
  const { user, isLoaded } = useUser();

  if (!isLoaded) return <LoadingSpinner />;
  if (!user) return <RedirectToSignIn />;

  return <PortalShell userId={user.id} />;
}

7.4 patient-api-access-client.tsx

The file web/src/app/(app)/(patient)/portal/settings/api-access/patient-api-access-client.tsx is the client component handling patient API token generation. It uses useAuth() to verify the session exists before rendering the token creation UI and sending requests to the backend. See BR-014.


8. Row-Level Security Patterns

Currently does not use PostgreSQL's native ROW SECURITY policies (RLS was not found in the schema DDL). Instead, row-level data isolation is enforced in application code through mandatory WHERE clause injection.

8.1 Organization-Level Isolation

Every query against hospitals, file_registry, and related tables includes an organization_id filter derived from the authenticated user's active Clerk organization:

Query pattern:
  SELECT * FROM hospitals
  WHERE organization_id = (
    SELECT id FROM organizations WHERE clerk_org_id = :clerk_org_id
  )
  AND is_active = true;

See BR-008.

8.2 Patient Record Isolation

Medical records in the PHI Cloud SQL instance are accessed only through the FastAPI pipeline backend. The Next.js web app proxies requests with the user's Clerk JWT, and the FastAPI service validates the token and maps userId to the patient's mrn_hash before returning records. Direct cross-patient queries are architecturally impossible from the web tier.

/api/facilities/:id/medical-records  →  FastAPI /records?facility_id=:id
                                         FastAPI validates: user org matches facility org

See BR-009.

8.3 File Registry Isolation

The file_registry table's organization_id and uploaded_by columns provide two levels of row isolation:

  • hie_admin / hie_member: see all files in their HIE organization
  • facility_admin / facility_member: see only files uploaded to their facility
  • platform_admin: sees all files across all organizations

See BR-010.


9. API Key Scopes

Currently exposes two API key surfaces, both visible in the UI component tree.

9.1 HIE API Keys

Managed via web/src/app/(app)/(hie)/dashboard/network/settings/api-keys/ and the platform route /api/v1/admin/api-keys.

ScopeDescriptionWho Can Create
hie:readRead-only access to HIE network datahie_admin
hie:writeUpload files, trigger processing jobshie_admin
hie:adminFull HIE management including user opsplatform_admin only
platform:adminPlatform-wide accessplatform_admin only

API key metrics are available at /api/v1/admin/api-keys/metrics/platform (platform admin only).

9.2 Patient API Access Tokens

Managed via web/src/app/(app)/(patient)/portal/settings/api-access/. Patients generate personal access tokens with a single scope:

ScopeDescriptionWho Can Create
patient:readRead-only access to own records and FHIR bundlesAuthenticated patient only

See BR-014 and BR-015.

9.3 CLI Authentication

The CLI (`