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

# ADR-018: PF→PM Encounter Reference Posture (No DB FK; Application-Layer Validation)

> PF-100 (Platform Ambient Transcription & Note Generation) introduces PF-owned tables — primarily pf_transcription_sessions — that capture an encounter_id refer…

**Status:** Proposed
**Date:** 2026-04-23
**Participants:** Platform Architecture Team, PF-100 Spec Owners, CL DX Lead, Compliance Officer (review)

***

## Context

PF-100 (Platform Ambient Transcription & Note Generation) introduces PF-owned tables — primarily `pf_transcription_sessions` — that capture an `encounter_id` referencing `pm_encounters.id`. The encounter is the canonical entity that ties scheduling (PM), clinical documentation (CL), and billing (PM) together; PF-100 sessions, when used in a clinical context, must be associatable with that encounter so downstream consumers (CL projection VIEWs, billing adapters, audit) can join on it.

Two prior ADRs frame the boundary:

* **ADR-002** (CL→PM cross-core FK): Allows a **scoped exception** for CL tables to add a database FK to `pm_encounters.id` with `ON DELETE RESTRICT`. The exception is explicitly limited to CL tables and the encounter entity. ADR-002 §Scope: *"No other cross-core foreign key is permitted without a separate ADR."*
* **ADR-005** / **ADR-006** (cross-core FKs to `pm_patients` / `hr_employees`): Each scoped to a single referenced entity from a single source core, on the same RESTRICT pattern.

The PF-100 spec (§7.1) initially proposed `encounter_id UUID … (FK enforced via trigger; ADR-002)`. The PF-100 integration document (§5) contradicted that, asserting PF→PM is **not** under the ADR-002 exception. Pre-verification (PF-100 doc-fix plan, 2026-04-23) flagged this as an architecture blocker requiring an explicit decision: either extend the cross-core-FK exception to cover PF→PM, or codify the no-FK posture and the application-layer validation that replaces it.

Forces at play:

1. **Referential integrity vs. core boundary discipline.** A DB FK is the strongest integrity guarantee; allowing one expands the surface of cross-core coupling that ADR-002 was deliberately narrow about.
2. **Source-of-truth ownership.** `pm_encounters` is owned and lifecycle-managed by PM. PF is platform-foundational and conceptually *upstream* of every domain core; introducing a downward FK from PF→PM inverts the dependency direction the constitution establishes (cores depend on PF, not the reverse).
3. **Lifecycle decoupling.** Ambient transcription sessions can be created in non-clinical contexts (HR, GR, CE, RH) where there is no `pm_encounters` row at all. `encounter_id` on `pf_transcription_sessions` is **nullable**. A FK with RESTRICT is still compatible with NULL, but the asymmetry (sometimes-FK, sometimes-not) adds cognitive load and creates a precedent that any PF table touching encounter-adjacent context could request the same exception.
4. **Trigger-enforced FKs are not a meaningful middle ground.** A `BEFORE INSERT/UPDATE` trigger that does `SELECT 1 FROM pm_encounters WHERE id = NEW.encounter_id` provides weaker guarantees than a real FK (no cascade semantics, no planner awareness, no automatic index requirement, races under concurrent delete) at higher cost (custom code path, separate test surface, easy to forget on `UPDATE`). It is the worst of both worlds.
5. **CL projection compatibility.** PF-100 Phase 3 projects `cl_ambient_*` tables as VIEWs over `pf_transcription_*`. The existing CL→PM FK on the legacy CL tables is removed when those tables become VIEWs (a VIEW cannot carry an FK). Integrity for the CL surface is preserved by (a) the application-layer validation on PF-100 session creation, and (b) the existing CL→PM FKs that remain on sibling CL tables (`cl_progress_notes`, etc.) which reference the same encounter and are written by the same workflows.

## Options Considered

### Option A: Extend ADR-002 to cover PF→`pm_encounters` (DB FK with RESTRICT)

* **How it works:** `ALTER TABLE pf_transcription_sessions ADD CONSTRAINT fk_pf_trans_sessions_encounter FOREIGN KEY (encounter_id) REFERENCES pm_encounters(id) ON DELETE RESTRICT;` Index on `encounter_id`. RLS unchanged (tenant isolation already enforced on both sides).
* **Pros:**
  * Strongest integrity guarantee.
  * Mirrors ADR-002's pattern; reviewers already understand it.
  * Database-level prevention of orphaned PF transcription sessions referencing deleted encounters.
* **Cons:**
  * Inverts the PF→core dependency direction the constitution establishes (PF should not depend on PM).
  * Establishes a precedent: any future PF table touching encounter context (PF-67 messaging, PF-91 compliance evidence, PF-89 API audit, etc.) will cite this ADR to request the same exception, eroding ADR-002's narrow scope.
  * Migration ordering: PM must run before PF in deployments — a reversal of the normal "PF first" ordering that introduces deployment risk.
* **Why not chosen:** Violates the architectural direction of the dependency graph; the precedent risk outweighs the integrity benefit, especially given the application-layer mitigations available.

### Option B: Trigger-enforced FK (PF→PM via `BEFORE INSERT/UPDATE` trigger)

* **How it works:** Custom PL/pgSQL trigger function checks `pm_encounters.id` exists and shares `organization_id`; raises on failure. Applied to INSERT and UPDATE on `pf_transcription_sessions.encounter_id`.
* **Pros:** Provides a synchronous existence check without declaring a formal FK constraint.
* **Cons:**
  * Weaker than a real FK: no planner awareness, no cascade semantics, no automatic index, race window under concurrent delete (unless `SELECT … FOR SHARE`, which blocks).
  * Custom code surface that must be unit-tested separately and is easy to drop accidentally during refactors.
  * Does not solve the dependency-direction objection of Option A; arguably makes it worse by hiding the dependency in a trigger body rather than declaring it openly.
  * Provides false reassurance ("we have integrity") without the actual semantic guarantees of an FK.
* **Why not chosen:** Worst of both worlds — pays the coupling cost of Option A without the integrity benefit.

### Option C (Chosen) ✓: No DB constraint; application-layer validation; opportunistic background reconciliation

* **How it works:**
  * `pf_transcription_sessions.encounter_id` is `UUID NULL` with **no FK constraint** and no trigger.
  * The session-creation Edge Function (PF-100 `pf-transcription-session-start`) validates that `encounter_id`, when provided, references an existing `pm_encounters` row in the same `organization_id` before inserting. On failure it returns a sanitized 400 to the caller.
  * A nightly reconciliation job (PF-100 `pf-transcription-reconcile`) flags rows with `encounter_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM pm_encounters WHERE id = encounter_id AND organization_id = pf_transcription_sessions.organization_id)` and emits a `pf.transcription.session.encounter_orphaned` event for the audit pipeline.
  * The CL projection VIEW (`cl_ambient_sessions`) does **not** declare an FK (VIEWs cannot). Integrity for the CL clinical surface is preserved by sibling CL tables (`cl_progress_notes.encounter_id`, etc.) that retain their existing CL→PM FKs under ADR-002.
  * PM encounter deletion remains controlled by PM's own workflows; deleting an encounter that has linked PF transcription sessions does not block at the DB level but will surface in the reconciliation report and via the `pm.encounter.deleted` event PF subscribes to (PF-100 §9.3).
* **Pros:**
  * Preserves PF→core dependency direction (PF does not depend on PM at the schema level).
  * Keeps ADR-002's exception narrow and defensible (CL→PM only).
  * No migration-order coupling between PF and PM beyond what already exists for `auth.users` references.
  * Nullable `encounter_id` for non-clinical PF-100 use cases (HR, GR, CE, RH ambient capture) is naturally accommodated without per-context branching.
  * Clear, auditable failure path: validation rejects bad inserts at the API; reconciliation catches drift.
* **Cons:**
  * Weaker integrity than Option A; an orphaned `encounter_id` can theoretically appear if (a) the validation path is bypassed (direct DB write) or (b) an encounter is deleted after a session references it.
  * Validation must be repeated in every write path (Edge Functions, future imports, future admin tools); risk of a path forgetting it.
* **Why chosen:** The application-layer mitigations (validation + reconciliation + event-driven cleanup) close the practical risk without inverting the dependency graph or establishing a precedent that would hollow out ADR-002 over time. The existing CL→PM FKs on sibling tables provide the strong guarantee where it matters most (clinical documentation linkage); PF-100's transcription session is a derivative, recoverable artifact whose orphaning is detectable and remediable rather than catastrophic.

## Decision

**PF→PM references (specifically `pf_transcription_sessions.encounter_id` → `pm_encounters.id`, and any future PF column referencing a PM-owned entity) are UUID columns with NO database foreign key constraint and NO trigger-based existence check. Validation is performed at the application layer in the originating Edge Function or RPC; an opportunistic background reconciliation job detects and reports drift.**

Specifically:

1. PF-100 migrations MUST declare `encounter_id UUID NULL` on `pf_transcription_sessions` with no FK and no trigger.
2. PF-100 MUST index `encounter_id` for query performance (`CREATE INDEX idx_pf_trans_sessions_encounter_id ON pf_transcription_sessions(encounter_id) WHERE encounter_id IS NOT NULL;`).
3. The session-creation Edge Function MUST validate `encounter_id` against `pm_encounters` (matching `organization_id`) before insert and return a sanitized error on failure.
4. PF-100 MUST ship a reconciliation job that emits `pf.transcription.session.encounter_orphaned` events for orphaned references.
5. PF MUST subscribe to `pm.encounter.deleted` and quarantine affected sessions per PF-100 spec §9.3.
6. ADR-002's scope is reaffirmed: the cross-core FK exception remains limited to CL→PM (encounter) and the entity-specific exceptions in ADR-005 / ADR-006. **No other cross-core FK is permitted without a separate, entity-scoped ADR.**

This decision applies to all current and future PF→domain-core references unless a future ADR explicitly extends the FK exception to a named PF table and a named referenced entity.

## Consequences

### Positive

* Preserves the constitution's PF→core dependency direction at the schema level.
* Keeps ADR-002 narrow and defensible; future requests to extend cross-core FKs face the same explicit ADR bar.
* No PF↔PM deployment-order coupling; PF migrations remain runnable ahead of PM.
* Nullable `encounter_id` accommodates non-clinical ambient capture (HR/GR/CE/RH) without schema branching.
* Clear, testable validation and reconciliation surface; drift is observable, not silent.

### Negative

* Weaker integrity than a DB FK: orphaning is possible in principle and must be caught by reconciliation rather than prevented at write time.
* Validation logic must be implemented (and maintained) in every write path. New write paths added in future phases (imports, admin tools, partner integrations) must remember to call the validator.
* Reconciliation job adds an operational surface (cron schedule, alerting on `pf.transcription.session.encounter_orphaned`).

### Mitigations

* Centralize the validator in `supabase/functions/_shared/pm-encounter.ts` (`assertEncounterExists(orgId, encounterId)`) so every write path imports the same function. Code review checklist for PF-100 includes "calls `assertEncounterExists` before insert/update of `encounter_id`".
* Add a unit test that fails CI if any PF Edge Function writes to `pf_transcription_sessions.encounter_id` without invoking the shared validator (AST-level check, similar to existing `verifyOrgAccess` enforcement).
* Reconciliation job runs nightly; alert threshold = any non-zero count emits a P3 ticket. SLA: drift resolved or sessions archived within 7 days.
* Document the no-FK posture in PF-100 spec §7.1 and integration doc §5 (already updated 2026-04-23 per doc-fix plan).
* Revisit this ADR if (a) reconciliation surfaces sustained drift > 0.1% of sessions over 90 days, or (b) a future PF feature requires synchronous integrity guarantees that application-layer validation cannot provide.

## Related Documents

* [ADR-002](/architecture/decisions/ADR-002-cl-pm-cross-core-foreign-keys) — CL→PM cross-core FK exception (this ADR reaffirms its narrow scope)
* [ADR-005](/architecture/decisions/ADR-005-cross-core-fk-pm-patients) — Cross-core FKs to `pm_patients`
* [ADR-006](/architecture/decisions/ADR-006-cross-core-fk-hr-employees) — Cross-core FKs to `hr_employees`
* [ADR-010](/architecture/decisions/ADR-010-core-pf-dependency-boundary) — Core/PF dependency boundary (constitutional basis for "PF does not depend on cores")
* [Constitution §1.2](https://github.com/Encore-OS/encoreos/blob/development/constitution.md) — Cross-core database references and CL-PM exception
* [Constitution §5.2.7](https://github.com/Encore-OS/encoreos/blob/development/constitution.md) — Cross-Core Database References
* [PF-100 Spec §7.1](https://github.com/Encore-OS/encoreos/blob/development/specs/pf/specs/PF-100-platform-ambient-transcription-and-note-generation.md) — `pf_transcription_sessions` schema
* [PF-100 Integration §5](/architecture/integrations/platform-ambient-transcription-integration) — Cross-core references and FK posture
* [PF-100 Integration §9.3](/architecture/integrations/platform-ambient-transcription-integration) — `pm.encounter.deleted` consumption and quarantine flow
* [ADR Template](https://github.com/Encore-OS/encoreos/blob/development/specs/_templates/ADR_TEMPLATE.md) — Format for future ADRs

***

**Approval required from:** Platform Architecture Team (chair), PF-100 spec owner, CL DX Lead (acknowledges no change to CL→PM FKs on sibling tables), Compliance Officer (acknowledges reconciliation/audit posture is sufficient for documentation integrity under HIPAA §164.312(c)(1)).

**On approval:** Update status to `Accepted`, record approval date, and link this ADR from PF-100 spec §7.1 and integration doc §5 in place of the prior ad-hoc explanations.
