Skip to main content

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)

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

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.