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

# Patient Portal & Self-Service — Integration Contracts

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

**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

***

## 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:**

| 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`
