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
- Role Hierarchy
- Permission Matrix
- Clerk Integration Architecture
- Protected Route Inventory
- Middleware Configuration
- Server-Side Auth Patterns
- Client-Side Auth Guards
- Row-Level Security Patterns
- API Key Scopes
- Audit Logging
- 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
| Scope | Route Prefix | Clerk Org Context | Description |
|---|---|---|---|
| Platform Admin | /admin | Platform-level org | Currently employees; full system control |
| HIE Network | /dashboard/network | HIE organization | Health Information Exchange operators |
| Provider/Facility | /dashboard | Facility organization | Hospital and clinic staff |
| Patient Portal | /portal | Per-patient identity | Patients 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
| Resource | Action | platform_admin | hie_admin | hie_member | facility_admin | facility_member | patient |
|---|---|---|---|---|---|---|---|
| Organizations | create | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| read (all) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| read (own) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
| update | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | |
| delete | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| approve | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| Hospitals / Facilities | create | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| read (all) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | |
| read (own org) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
| update | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | |
| link to HIE | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | |
| Users | invite (facility) | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| invite (patient) | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | |
| list (org) | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | |
| deactivate | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | |
| Medical Records | read (any org) | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| read (own facility) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
| read (own patient) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | |
| write / upload | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
| export | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | |
| Files | upload | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| read metadata | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | |
| API Keys | create (platform) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| create (hie-scoped) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | |
| create (patient) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | |
| read metrics | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | |
| revoke | ✅ | ✅ (own) | ❌ | ❌ | ❌ | ✅ (own) | |
| Pricing / Revenue | read | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| update tiers | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| Subscriptions | checkout | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ |
| cancel | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | |
| PHI Audit Log | read | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Analytics | platform-wide | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| hie-scoped | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | |
| Registrations | submit (hie) | public | — | — | — | — | — |
| submit (facility) | public | — | — | — | — | — | |
| review / approve | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| Provider Approval | approve | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| ATS | read | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Session | read 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 Role | Clerk Organization Type | Currently App Role |
|---|---|---|
org:admin | Platform org (Currently internal) | platform_admin |
org:member | Platform org | platform_member (not currently used) |
org:admin | HIE organization | hie_admin |
org:member | HIE organization | hie_member |
org:admin | Facility organization | facility_admin |
org:member | Facility organization | facility_member |
| N/A (individual user) | None | patient |
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).
| Route | File | Method | Required Role | Notes |
|---|---|---|---|---|
/api/admin/facility-orgs/create | admin/facility-orgs/create/route.ts | POST | platform_admin | Creates a facility organization record and provisions Clerk org |
/api/admin/invitations/facility-user | admin/invitations/facility-user/route.ts | POST | platform_admin | Sends facility user invitation email |
/api/admin/invitations/patient | admin/invitations/patient/route.ts | POST | platform_admin | Sends patient portal invitation |
/api/admin/org-links/approve | admin/org-links/approve/route.ts | POST | platform_admin | Approves a facility-to-HIE linkage request |
/api/admin/pricing/tiers | admin/pricing/tiers/route.ts | GET, POST, PUT | platform_admin | Manages subscription pricing tiers |
/api/admin/registrations/facility | admin/registrations/facility/route.ts | GET | platform_admin | Lists pending facility registrations |
/api/admin/registrations/facility/:id | admin/registrations/facility/[id]/route.ts | GET, PATCH | platform_admin | Reviews and approves a specific facility registration |
/api/admin/registrations/facility/hie-initiate | admin/registrations/facility/hie-initiate/route.ts | POST | platform_admin | Initiates HIE-side facility onboarding |
/api/admin/registrations/hie | admin/registrations/hie/route.ts | GET | platform_admin | Lists pending HIE registrations |
/api/admin/registrations/hie/:id | admin/registrations/hie/[id]/route.ts | GET, PATCH | platform_admin | Reviews and approves a specific HIE registration |
/api/admin/revenue/stats | admin/revenue/stats/route.ts | GET | platform_admin | Platform-wide revenue dashboard data |
/api/v1/admin/analytics/by-org | v1/admin/analytics/by-org/route.ts | GET | platform_admin | Cross-org analytics aggregation |
/api/v1/admin/api-keys | v1/admin/api-keys/route.ts | GET, POST, DELETE | platform_admin | Platform-level API key management |
/api/v1/admin/api-keys/metrics/platform | v1/admin/api-keys/metrics/platform/route.ts | GET | platform_admin | Aggregate API key usage metrics |
/api/v1/admin/approve-provider | v1/admin/approve-provider/route.ts | POST | platform_admin | Approves a provider account |
/api/v1/admin/audit/phi-access | v1/admin/audit/phi-access/route.ts | GET | platform_admin | PHI 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.
| Route | File | Method | Required Role | Notes |
|---|---|---|---|---|
/api/hies | hies/route.ts | GET | hie_admin, platform_admin | Lists all HIE organizations |
/api/hies/:id | hies/[id]/route.ts | GET, PATCH | hie_admin, platform_admin | Gets or updates a specific HIE |
/api/orgs/hie | orgs/hie/route.ts | GET | hie_admin, hie_member | Returns the caller's HIE organization |
/api/facilities | facilities/route.ts | GET, POST | hie_admin, platform_admin | Lists or creates facilities within an HIE |
/api/facilities/:id | facilities/[id]/route.ts | GET, PATCH | hie_admin, hie_member, platform_admin | Facility detail |
/api/facilities/:id/hospital | facilities/[id]/hospital/route.ts | GET | hie_admin, hie_member, facility_admin, facility_member | Hospital record for facility |
/api/facilities/:id/medical-records | facilities/[id]/medical-records/route.ts | GET | hie_admin, hie_member, platform_admin | Medical records scoped to facility |
/api/facility/link-to-hie | facility/link-to-hie/route.ts | POST | facility_admin, platform_admin | Requests facility-to-HIE linkage |
/api/hospital/by-facility-org | hospital/by-facility-org/route.ts | GET | facility_admin, facility_member, hie_admin, hie_member | Resolves hospital from org context |
/api/files | files/route.ts | GET, POST | hie_admin, hie_member, facility_admin, facility_member | File registry access |
/api/subscriptions/checkout | subscriptions/checkout/route.ts | POST | hie_admin, facility_admin | Initiates Stripe checkout |
/api/subscriptions/cancel | subscriptions/cancel/route.ts | POST | hie_admin, facility_admin | Cancels active subscription |
4.3 Patient Routes — patient (authenticated individual) Required
| Route | File | Method | Required Role | Notes |
|---|---|---|---|---|
/api/onboarding/patient | onboarding/patient/route.ts | POST | Authenticated user | Patient onboarding form submission |
/api/session | session/route.ts | GET | Any authenticated user | Returns current session metadata |
4.4 Shared Auth Routes
| Route | File | Method | Auth Required | Notes |
|---|---|---|---|---|
/api/auth/sync-user | auth/sync-user/route.ts | POST | Clerk webhook secret | Clerk webhook; verified via svix signature |
/api/invitations/:token | invitations/[token]/route.ts | GET | None | Token-based invitation lookup (unauthenticated) |
/api/invitations | invitations/route.ts | POST | facility_admin, platform_admin | Sends invitation; role checked server-side |
/api/npi/verify | npi/verify/route.ts | GET | Any authenticated user | NPI registry lookup; requires login but no specific role |
/api/organizations/approved | organizations/approved/route.ts | GET | Any authenticated user | Returns approved orgs for dropdowns |
/api/organizations/check-hie | organizations/check-hie/route.ts | GET | Any authenticated user | Checks HIE membership status |
/api/registrations/hie | registrations/hie/route.ts | POST | None (public) | Public HIE self-registration form |
/api/registrations/facility | registrations/facility/route.ts | POST | None (public) | Public facility self-registration form |
4.5 Public / Health Routes
| Route | File | Method | Auth Required | Notes |
|---|---|---|---|---|
/api/health | health/route.ts | GET | None | Liveness check; no auth |
/api/dev/email-preview | dev/email-preview/route.ts | GET | Development env only | Preview email templates; blocked in production |
4.6 UI Page Route Protection
| Page Route | Shell | Required Role | Clerk Component |
|---|---|---|---|
/admin/* | Platform Admin | platform_admin | auth() server-side + middleware redirect |
/admin/analytics | Platform Admin | platform_admin | Server component auth check |
/admin/ats/* | Platform Admin | platform_admin | Server component auth check |
/admin/api-docs | Platform Admin | platform_admin | platform-admin-shell.tsx gate |
/dashboard/network/* | HIE Network | hie_admin, hie_member | Middleware + layout auth check |
/dashboard/network/settings/api-keys | HIE Network | hie_admin | Admin-only UI gate |
/dashboard/network/settings/modules | HIE Network | hie_admin | Admin-only UI gate |
/dashboard/network/approvals | HIE Network | hie_admin | Admin-only UI gate |
/dashboard/network/export | HIE Network | hie_admin | Admin-only UI gate |
/portal/* | Patient | Authenticated (no org) | portal/layout.tsx auth check |
/portal/settings/api-access | Patient | Authenticated | patient-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 organizationfacility_admin/facility_member: see only files uploaded to their facilityplatform_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.
| Scope | Description | Who Can Create |
|---|---|---|
hie:read | Read-only access to HIE network data | hie_admin |
hie:write | Upload files, trigger processing jobs | hie_admin |
hie:admin | Full HIE management including user ops | platform_admin only |
platform:admin | Platform-wide access | platform_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:
| Scope | Description | Who Can Create |
|---|---|---|
patient:read | Read-only access to own records and FHIR bundles | Authenticated patient only |
See BR-014 and BR-015.
9.3 CLI Authentication
The CLI (`