Skip to main content
Version: 1.1.0 Last Updated: 2026-02-23 Status: Complete (all features including T11 Clinical Record Access)

Overview

PM-12 Patient Portal provides a self-service interface for patients. It integrates with multiple PM and PF specs via the Platform Integration Layer—no direct cross-core imports.

Integration Matrix


Contract: Self-Scheduling (PM-03)

Hook: usePortalAppointments Source: src/cores/pm/hooks/usePortalAppointments.ts Data Access:
  • Table: pm_appointments
  • Filter: .eq('organization_id', orgId).eq('patient_id', patientId)
  • Scope: patientId from usePortalPatientId() (enforces proxy scope)
Operations: Constraints:
  • Cancellation only for status = 'scheduled'
  • No direct booking of new slots (future enhancement)
  • RLS enforces patient isolation at DB level

Contract: Intake/Consent Forms (PF-08)

Page: PortalFormsPage Source: src/cores/pm/pages/portal/PortalFormsPage.tsx Integration Pattern:
  • Uses FormEmbed from @/platform/forms (Platform Integration Layer)
  • Patient context injected from usePortalPatientId()
  • Form submissions stored in fw_form_submissions with patient linkage
Operations:

Contract: Demographic Updates (PM-01)

Hook: usePortalDemographicRequests Source: src/cores/pm/hooks/usePortalDemographicRequests.ts Data Access:
  • Read: pm_patients (current demographics, read-only display)
  • Write: fw_form_submissions (update request submission)
Operations:

Contract: Billing & Payments (PM-09)

Hook: usePortalBilling Source: src/cores/pm/hooks/usePortalBilling.ts Data Access:
  • Tables: pm_charges, pm_payments
  • Filter: .eq('organization_id', orgId).eq('patient_id', patientId)
Operations: Constraints:
  • No direct card data handling (PCI compliance via existing PM-09 flow)
  • Balance is calculated client-side from total_amount columns

Contract: Secure Messaging (PM-14)

Hook: usePortalMessaging Source: src/cores/pm/hooks/usePortalMessaging.ts Integration Pattern:
  • Thin wrapper around existing PM-14 hooks
  • Enforces senderType: 'patient' on all outbound messages
  • Conversations filtered by patient_id from usePortalPatientId()
Operations:

Contract: Clinical Record Access (CL-*) — ✅ Implemented

Hook: usePortalClinicalSummary Source: src/cores/pm/hooks/usePortalClinicalSummary.ts Page: PortalClinicalPage Source: src/cores/pm/pages/portal/PortalClinicalPage.tsx Integration Pattern:
  • Read-only access to clinical chart, medications, and treatment goals
  • Consent enforcement via useConsentCheck from @/platform/clinical (Platform Integration Layer)
  • SUD-indicated data gated by CL-11 cl_check_sud_consent RPC
  • Non-SUD data always accessible (21st Century Cures Act compliance)
  • Patient context from usePortalPatientId() (proxy-aware)
Data Access:
  • Tables: cl_patient_charts, cl_medications, cl_treatment_goals
  • Filter: .eq('patient_id', patientId) from usePortalPatientId()
Operations: Consent Enforcement:
  • If cl_patient_charts.flags.sud_indicated is true, calls cl_check_sud_consent RPC
  • Without consent: diagnosis-level data suppressed, medications and goals still visible (non-SUD)
  • Consent banner displayed when data is restricted

Security Architecture

Authentication

  • Supabase Auth (email/password + TOTP MFA)
  • Portal sessions audited in pm_portal_sessions (append-only)
  • Account lockout after 5 failed attempts

Data Isolation

  • RLS Layer: pm_portal_has_org_access() and pm_portal_visible_patient_ids() (SECURITY DEFINER)
  • Application Layer: usePortalPatientId() hook enforces scope on all queries
  • Proxy Support: Active, non-expired proxy relationships only

Tables with FORCE ROW LEVEL SECURITY

  • pm_portal_users
  • pm_portal_sessions