Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
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
| Integration | Spec | Direction | Pattern | Status |
|---|
| Self-Scheduling | PM-03 | Portal → PM | Direct PM query (same core) | ✅ Complete |
| Intake/Consent Forms | PF-08 | Portal → PF | Platform Integration Layer | ✅ Complete |
| Demographic Updates | PM-01 | Portal → PM | Direct PM query + form submission | ✅ Complete |
| Billing & Payments | PM-09 | Portal → PM | Direct PM query (same core) | ✅ Complete |
| Secure Messaging | PM-14 | Portal → PM | Direct PM hooks (same core) | ✅ Complete |
| Clinical Records | CL-* | Portal → CL | Platform Integration Layer | ✅ Complete |
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:
| Operation | Method | Auth | Tenant Filter |
|---|
| List upcoming | SELECT | Portal user | org_id + patient_id |
| List past | SELECT | Portal user | org_id + patient_id |
| Cancel | UPDATE (status → cancelled) | Portal user | org_id + patient_id + appointment_id |
Constraints:
- Cancellation only for
status = 'scheduled'
- No direct booking of new slots (future enhancement)
- RLS enforces patient isolation at DB level
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:
| Operation | Method | Auth | Notes |
|---|
| List available forms | Platform forms API | Portal user | Filtered by owningCore |
| Submit form | Platform forms API | Portal user | Linked to patient_id |
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:
| Operation | Method | Auth | Notes |
|---|
| View demographics | SELECT on pm_patients | Portal user | Read-only, scoped by patient_id |
| Submit update request | INSERT on fw_form_submissions | Portal user | Staff reviews via PM-01 workflow |
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:
| Operation | Method | Auth | Notes |
|---|
| View balance | SELECT (charges - payments) | Portal user | Aggregated from pm_charges and pm_payments |
| View payment history | SELECT on pm_payments | Portal user | Scoped by patient_id |
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:
| Operation | Method | Auth | Notes |
|---|
| List conversations | PM-14 hooks | Portal user | Patient-visible only |
| View thread | PM-14 hooks | Portal user | Scoped by conversation participant |
| Send message | PM-14 hooks | Portal user | senderType forced to ‘patient’ |
| Create conversation | PM-14 hooks | Portal user | Category defaults set |
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:
| Operation | Method | Auth | Notes |
|---|
| View chart summary | SELECT on cl_patient_charts | Portal user | Read-only, scoped by patient_id |
| View medications | SELECT on cl_medications | Portal user | Active/on_hold only |
| View treatment goals | SELECT on cl_treatment_goals | Portal user | Active/in_progress only |
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