Принцип в одну фразу: между 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 не нужен. Решим если/когда задача появится.
Связано
- phi-in-fhir-not-sql — сестринский принцип: PHI в покое
- inngest — оркестратор и его лимиты
- biomarker-graph — где живут opaque graph-IDs
- biomarker-actuality-integration — первый concrete consumer
- fhir-resource-origin-and-lifecycle — общая система пометки служебных FHIR-ресурсов
Сноски
-
inngest § «4 MB step output… медицинские данные пациентов не должны оседать в логах / трейсах оркестратора» — сегодняшняя тактика, источник этой формализации. ↩
-
Сессия
ildar/<sid>, 2026-05-13 — поднял ограничение «Inngest не под BAA», предложил opaque IDs графу как general solution. Carry-over: записать конкретный sid8. ↩