Сегодня — A (каждый frontend сам). Решение не было принято осознанно: первый next.js API-route скопировал паттерн «direct inngest.send», b2b-api сделал то же со своей стороны, в комментарии у app/api/summary/route.ts зафиксирован выбор одной строкой («Inlined — no b2b-api hop»), но без таблицы альтернатив. Эта страница — место чтобы взвесить варианты явно и решить надо ли что-то менять.
Контекст
В коде сейчас две дублирующиеся точки входа на одно событие analysis/patient-summary.requested:
- b2c-dashboard — Next.js API-route с WorkOS
withAuth, делает Prisma-записьAnalysisRunи шлёт event через тонкий wrappersendInngestEvent(untyped). - b2b-api — Hono-роут на Bearer API-key, делает то же самое через локальный
Inngest-клиент с типизированнымиEventSchemas.
Те же дубли есть для остальных events (inngest): input/pdf.uploaded, input/recognize.uploaded, input/json.uploaded, analysis/patient-summary.requested. Каждый раз — две edge-функции, два validation-блока, два места эмиссии.
analysis-worker (где createFunction-ы) — internal-only, публичного HTTP-API не имеет. Все три приложения (b2c-dashboard, b2b-api, analysis-worker) — отдельные deploys в одном monorepo, между собой общаются только через Inngest events (никаких прямых HTTP-вызовов сегодня нет).
Соседний вопрос — inngest-pipeline-orchestration-vs-choreography — про композицию внутри pipeline (после первого event’а). Эта же страница — про entry-границу: кто и где этот первый event эмитит.
Позиции
A. Каждый frontend эмитит сам (status quo)
Каждый Next.js / Hono API-route делает auth + Prisma + inngest.send. b2c-dashboard говорит с Inngest напрямую, b2b-api — тоже напрямую, patient-portal в будущем — тоже сам.
- За: ноль extra HTTP-хопов; нет нового сервиса; auth/org-resolution естественно живёт в edge приложения (WorkOS-сессия / Bearer-key) — где она и так есть; deploy-граф плоский. Хорошо ложится на «frontend = backend-for-frontend», что и так case.
- Против: валидация payload + создание
AnalysisRun+inngest.sendдублируются в двух (потенциально N) приложениях; event-shape утечёт изменением в b2c но не в b2b и наоборот — drift; зависимость отinngestSDK прорастает в каждое web-приложение; observability (логирование/трейсинг эмиссии) надо повторять; в b2c — untyped wrapper, в b2b — typed schemas, формальная согласованность типа не гарантирована.
B. b2b-api как единый event-emission gateway
b2c-dashboard / patient-portal вызывают POST /api/v1/summary у b2b-api по HTTP; b2b-api держит auth по своим правилам и эмитит event. Frontend’ы становятся тонкими.
- За: один владелец event-shape (b2b-api
EventSchemas); типы единые; observability в одной точке; добавить новый канал (mobile, EHR-integration) — указать на тот же endpoint, без своегоinngest.send. b2b-api уже играет роль public API, у него Bearer-auth и rate-limit готовы. - Против: b2b-api — это внешний customer-facing API (Bearer-токены, SLA, billing-aware), не internal-backend. Использовать его как internal entry для своего b2c смешивает две роли: одна — продукт-API для клиентов, другая — internal coordination layer. Если изменим внешний API под внутренние нужды — сломаем клиентов; если будем делать всё backward-compat — internal сторона будет хромать. Плюс: b2c должен либо иметь свой Bearer-key (странно для internal), либо специальный internal-auth path → ещё один auth-режим в b2b-api.
C. Выделенный orchestration-API / backend-coordination service
Новый сервис (или новый модуль в analysis-worker) с одной ролью: accept HTTP + validate + emit Inngest. b2c-dashboard / b2b-api / future-mobile / future-EHR — все стучатся туда. b2b-api возвращает себе чистую external-customer-роль.
- За: чистое разделение «product-API для клиентов» (b2b-api) и «internal orchestration entry» (этот сервис); один владелец event-shape; observability централизована; добавление консьюмеров не трогает существующие сервисы.
- Против: новый deploy-юнит (CI, secrets, monitoring, on-call); auth-схема между internal-сервисами надо придумать (mTLS / shared secret / VPC-internal-only); один из вариантов реализации — это тоже сам Inngest (использовать
event-driven HTTP webhook → emit eventчерез Inngest features), что выглядит как круговая зависимость; для текущего масштаба (2 frontend’а, один event-flow) скорее over-engineering.
D. Shared workspace-пакет с типизированными emitter-ами (ОРТОГОНАЛЬНО)
@repo/pipeline-events экспортирует функции вида emitPatientSummaryRequested(payload) — typed wrapper над inngest.send. Каждое web-приложение импортирует и вызывает. Это про type coupling, не про layering — может комбинироваться с A, B или C.
- За A+D: type-safety без архитектурных изменений; устраняет drift между b2c untyped и b2b typed; cheap-est shift сегодня; единое место для observability-hook’а.
- Против A+D: auth-блок и Prisma-блок всё равно дублируются (это не про emit, это про вокруг); type-safety хорошо, но не отвечает на вопрос «нужен ли единый entry-point для будущих consumers».
- Против B+D / C+D: теряет смысл — type-safety автоматическая, если emitter живёт в одном месте.
D — самостоятельный «cheap win» вне зависимости от того, выберем ли мы A / B / C для structural-вопроса.
E. analysis-worker экспонирует HTTP-API (отброшено)
worker сам принимает HTTP-запросы и эмитит events изнутри. Сегодня worker строго internal, не имеет публичного network-входа, не имеет request-level auth (только Inngest-protocol). Превращать его в HTTP-сервис — большой сдвиг роли. Сохраняется как теоретическая возможность, но для нашего setup не лучше C, и переломывает текущую модель «worker = pipeline-runtime, не API».
Что нужно для разрешения
- Появятся ли в обозримой перспективе сторонние event-consumers (mobile app, EHR-integration, partner-API)? Если да — B vs C становится реальным вопросом; если нет — A+D скорее всего достаточно.
- Готовы ли разделять «external product-API» (b2b-api) и «internal orchestration entry»? Если да — B неудачный выбор (смешивает роли), смотрим на C; если нет — B приемлем.
- Есть ли у нас case’ы, где разные frontend’ы должны эмитить разные event-shapes на «то же самое» (например, b2c хочет привязать к user, b2b хочет привязать к org-without-user)? Если да — централизация в B/C усложняет адаптацию; если нет — централизация чисто выигрывает.
- Текущая боль реальная или гипотетическая? Конкретные эпизоды drift’а между b2c и b2b emission — есть ли они в git log? Без них «дублирование» — теоретический недостаток.
Решение зависит от ответа на эти вопросы — особенно первого.
Следствия
- Пока сидим в A, новые events дублируют существующий паттерн в обоих frontend’ах; стоимость drift’а растёт с каждым новым event-flow.
- D без A/B/C-выбора — самостоятельный шаг, который не блокируется этим решением. Если хотим типизировать сейчас — можно вынести
@repo/pipeline-eventsнезависимо. - Если выберем C, появится новый internal-сервис → нужно решить кто owns его (analysis-worker team? отдельный?), как deploy’ится (тот же ArgoCD-pattern), как auth между сервисами.
- Если выберем B, нужен новый auth-режим в b2b-api для internal-callers — без него b2c будет вынуждена держать Bearer-key, что не имеет смысла внутри сети.
Открытые вопросы
- Есть ли реальный drift между текущими emission-точками b2c и b2b на одном event’е? (TBD — diff
app/api/summary/route.tsиapps/b2b-api/src/modules/summary/route.tsпо полям event payload.) - Patient Portal как третий потенциальный emission-источник — он сейчас вообще эмитит events или только consumes? (TBD verify
apps/patient-portal/.) - Стоит ли D делать прямо сейчас как «cheap type-safety win», независимо от решения по A/B/C? Можно ли пере-использовать существующий
apps/b2b-api/src/modules/shared/inngest.service.tsEventSchemasкак seed для shared-пакета.
Связано
- inngest-pipeline-orchestration-vs-choreography — соседний вопрос про композицию внутри pipeline.
- inngest — каталог events и наш orchestrator (entity-страница).
- phi-not-in-orchestrator-payload — что внутри event payload (refs + opaque IDs, без PHI). Эта страница — про кто payload собирает.
- interpretation-scope-patient-vs-test — конкретный event
analysis/patient-summary.requested, в чьих route-ах сегодня живёт обсуждаемая логика.