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 — primarilypf_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.idwithON 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.
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:
- 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.
- Source-of-truth ownership.
pm_encountersis 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). - Lifecycle decoupling. Ambient transcription sessions can be created in non-clinical contexts (HR, GR, CE, RH) where there is no
pm_encountersrow at all.encounter_idonpf_transcription_sessionsis 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. - Trigger-enforced FKs are not a meaningful middle ground. A
BEFORE INSERT/UPDATEtrigger that doesSELECT 1 FROM pm_encounters WHERE id = NEW.encounter_idprovides 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 onUPDATE). It is the worst of both worlds. - CL projection compatibility. PF-100 Phase 3 projects
cl_ambient_*tables as VIEWs overpf_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 onencounter_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.idexists and sharesorganization_id; raises on failure. Applied to INSERT and UPDATE onpf_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.
- Weaker than a real FK: no planner awareness, no cascade semantics, no automatic index, race window under concurrent delete (unless
- 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_idisUUID NULLwith no FK constraint and no trigger.- The session-creation Edge Function (PF-100
pf-transcription-session-start) validates thatencounter_id, when provided, references an existingpm_encountersrow in the sameorganization_idbefore inserting. On failure it returns a sanitized 400 to the caller. - A nightly reconciliation job (PF-100
pf-transcription-reconcile) flags rows withencounter_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 apf.transcription.session.encounter_orphanedevent 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.deletedevent 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.usersreferences. - Nullable
encounter_idfor 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_idcan 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.
- Weaker integrity than Option A; an orphaned
- 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 (specificallypf_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:
- PF-100 migrations MUST declare
encounter_id UUID NULLonpf_transcription_sessionswith no FK and no trigger. - PF-100 MUST index
encounter_idfor query performance (CREATE INDEX idx_pf_trans_sessions_encounter_id ON pf_transcription_sessions(encounter_id) WHERE encounter_id IS NOT NULL;). - The session-creation Edge Function MUST validate
encounter_idagainstpm_encounters(matchingorganization_id) before insert and return a sanitized error on failure. - PF-100 MUST ship a reconciliation job that emits
pf.transcription.session.encounter_orphanedevents for orphaned references. - PF MUST subscribe to
pm.encounter.deletedand quarantine affected sessions per PF-100 spec §9.3. - 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.
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_idaccommodates 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 “callsassertEncounterExistsbefore insert/update ofencounter_id”. - Add a unit test that fails CI if any PF Edge Function writes to
pf_transcription_sessions.encounter_idwithout invoking the shared validator (AST-level check, similar to existingverifyOrgAccessenforcement). - 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 — CL→PM cross-core FK exception (this ADR reaffirms its narrow scope)
- ADR-005 — Cross-core FKs to
pm_patients - ADR-006 — Cross-core FKs to
hr_employees - ADR-010 — Core/PF dependency boundary (constitutional basis for “PF does not depend on cores”)
- Constitution §1.2 — Cross-core database references and CL-PM exception
- Constitution §5.2.7 — Cross-Core Database References
- PF-100 Spec §7.1 —
pf_transcription_sessionsschema - PF-100 Integration §5 — Cross-core references and FK posture
- PF-100 Integration §9.3 —
pm.encounter.deletedconsumption and quarantine flow - ADR Template — 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.