Принцип в одну фразу: между orchestrator-шагами (inngest сегодня; потенциально Temporal / self-hosted Inngest в будущем) идут только refs на ресурсы + непрозрачные ID + минимальная metadata — никогда не содержательное PHI (значения наблюдений, имена клинических состояний, prose-обзоры). Сестринское решение к phi-in-fhir-not-sql («где PHI живёт в покое»); эта страница — про где PHI живёт в передаче.

Контекст

inngest — наш текущий orchestrator. Step output и event payload хранятся в Inngest-инфраструктуре (state store + UI), и сегодня Inngest НЕ под BAA для нашего тарифа — платный HIPAA-tier существует, мы за него не платим. Self-hosted Inngest — отдельный workstream, который мы пока не делаем.

Carry-over: BAA-статус для всех vendor’ов в pipeline — отдельная страница (technical/baa-coverage или аналог), которая фиксирует «где мы под BAA, где нет, какие риски это создаёт». Здесь упомянуто только то, что для Inngest BAA нет → отсюда вытекает этот принцип.

Значит, всё PHI-содержимое, которое попадает в step output / event data:

  • оседает в логах оркестратора (видимо инженерами через Inngest UI)
  • находится за пределами нашего FHIR-access-control
  • в случае compromise инфраструктуры Inngest утечёт

Прямой ответ — минимизировать PHI-surface в payload оркестратора: не отдавать ему то, без чего downstream-стадии могут обойтись (а они могут — содержательное наполнение re-fetch’ат из FHIR по ID). Это не universal-truism «защищаем PHI везде», а конкретное правило про границу шагов: что физически складывается в Inngest state store ↔ что вообще не пересекает эту границу.

Тактически это правило уже сформулировано в inngest1 («через step output фактически бы дублировались в инфраструктуре оркестратора… через Inngest идут только ID + metadata») — эта страница вытаскивает его в самостоятельное решение, на которое могут опираться другие интеграции2.

Принцип

Через step output / event payload проходят:

  • FHIR resource refs (Patient/abc, Observation/xyz) — IDs существующих ресурсов в Healthcare API под нашим access-control. Сами по себе IDs не клиническое содержимое.
  • Opaque domain IDs (графовые node-IDs, LOINC-коды, SNOMED-коды) — указатели на статичные справочники (граф биомаркеров, словари). Без доступа к справочнику не содержат клинической информации.
  • Малая metadata (evidenceTier-enum, triage-enum, counts, runId, organizationId, patientId) — не содержит значений / диагнозов / препаратов.

Через step output НЕ проходят:

  • Значения наблюдений (Observation.value)
  • Текст диагнозов / препаратов (clinical text)
  • Inline-сгенерированные prose-обзоры с patient-specific содержимым (reasoning, clinicalInterpretation)
  • Демография в text-form (имена, адреса)

Downstream-стадии re-fetch’ат из FHIR / графа по ref / ID когда им нужно содержательное наполнение.

Где PHI ОК

  • В FHIR (Healthcare API) — primary store, под BAA с GCP, access-control строгий (phi-in-fhir-not-sql).
  • В памяти процесса (in-process variables внутри одного step.run) — runtime-only, не персистится в инфре orchestrator’а. PHI допустимо до тех пор, пока step.run не возвращает её в результат.
  • В GCS bucket’е под BAA (если бы мы создали отдельный intermediate store) — допустимо как state-bridge между Inngest steps, передавая только gs:// URL. Сегодня не используется для intermediate analytical artifacts, но вариант существует.

Где PHI НЕ ОК

  • Inngest step output / event payload — основной мотив этой страницы.
  • Логи / трейсы оркестратора — Langfuse под BAA, поэтому туда PHI допускается; Inngest UI — нет.
  • Postgres / Prisma — отдельная decision phi-in-fhir-not-sql.

Тактика — opaque IDs графу

Ключ графа отдельно обсуждается на biomarker-graph-storage (LOINC vs trending_group_id vs internal opaque IDs). Здесь не про окончательный выбор ключа графа, а про то что ходит через Inngest payload. Эти две оси независимы: если граф keyed by LOINC (deterministic, публичный код), на wire всё ещё можно гонять наши внутренние opaque-IDs через step boundary, а на чтении графа resolve их в LOINC уже в памяти. То есть граф-ключ — про storage / interop, opaque-ID-на-wire — про что попадает в Inngest UI.

Чтобы передавать через Inngest даже указатели на биомаркеры/состояния без утечки имён (которые сами по себе клиническая семантика), исторически предлагалось дать графовым нодам стабильные opaque IDs:

// биомаркер-граф
"biomarkers": {
  "Hemoglobin (Hb)": { "id": "g_b_001", "domain": "hematology", ... }
}

Inngest payload содержит g_b_001, не "Hemoglobin (Hb)". Расшифровка требует доступа к графу (под нашим контролем).

Dev-эргономика: в коде везде используем читаемые имена ("Hemoglobin"); encode/decode wrapper-функции на границе step boundary’а. В логах / debug UI — resolved-обратно к именам. То есть тяжесть «opaque-на-wire» несёт boundary-слой, а не сам граф; имена / LOINC внутри графа остаются как есть.

Когда принцип неприменим

  • One big in-process step (всё внутри одного step.run) — там PHI остаётся в памяти, через границу не идёт. Допустимо, теряет per-stage observability + granular retry — это trade-off.
  • Self-hosted / HIPAA-tier Inngest — если в будущем оркестратор будет под BAA, принцип всё ещё имеет смысл (defence in depth), но менее строгий.

Следствия для интеграций

  • Интеграция сервиса актуальности (biomarker-actuality-integration) — первый concrete consumer. GenerationReadyPackage (бывш. V2GenerationReadyPackage до Phase A rename) редуцируется до refs + graph-IDs до пересечения step boundary’а; downstream re-fetches значения из FHIR.
  • Будущие интеграции (если появятся другие LLM-pipeline’ы) — следуют тому же паттерну.
  • Encode/decode на boundary — небольшой инфра-кусок (wrapper-функции), не граф schema refactor.

Открытые вопросы

  • reasoning / triageRationale (LLM-генерируемый prose-текст) — упоминает patient-specific содержимое (значения, имена состояний). Куда складывать: omit на cross-step передаче, GCS-bucket, отдельный FHIR-resource (Provenance), или keep in-process если pipeline = один большой step. Сегодня — keep in-process в actuality-integration.
  • Generate opaque IDs deterministically или random? Hash-of-name даёт стабильность при regenerate, random — полностью opaque (нельзя восстановить имя если утёк ID без графа). См. biomarker-graph.
  • Нужен ли отдельный PHI-safe blob-store (GCS bucket под BAA) — если в будущем появятся пайплайны, где между Inngest-шагами надо передать большие intermediate-артефакты с PHI (а в payload нельзя). Сегодня все наши интеграции укладываются в refs + graph-IDs, blob-store не нужен. Решим если/когда задача появится.

Связано

Сноски

  1. inngest § «4 MB step output… медицинские данные пациентов не должны оседать в логах / трейсах оркестратора» — сегодняшняя тактика, источник этой формализации.

  2. Сессия ildar/<sid>, 2026-05-13 — поднял ограничение «Inngest не под BAA», предложил opaque IDs графу как general solution. Carry-over: записать конкретный sid8.