Сегодня — 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 через тонкий wrapper sendInngestEvent (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; зависимость от inngest SDK прорастает в каждое 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.ts EventSchemas как seed для shared-пакета.

Связано