docs(adr-0024): ai service relay — gRPC dial + SSE bridge + POC principal #194
Reference in New Issue
Block a user
Delete Branch "docs/adr-0024-ai-service-relay"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Proposes ADR-0024 — the integration contract between
apf_portal's BFF and the siblingapf-ai-servicerepository. The ADR bundles four tightly-coupled sub-choices: the wire transport between BFF and AI service, the wire transport between BFF and SPA for chat streaming, how the protos reach the BFF, and how user identity travels across the boundary in v1. Status:proposed. No code lands in this PR — the goal is to lock the contract before the implementation chantier starts.The chosen design:
@grpc/grpc-js, h2c in dev / h2 + TLS in prodtext/event-stream— one SSE frame perChatEventoneof caseRagService.Search+ModelsService.ListModelsapps/portal-bff/src/grpc/proto/apf-ai/,ts-protocodegen on demand, both.proto+ generated.tscommittedPrincipal { subject, roles[], attributes{} }in the proto body — mirrorsapf-ai-service's ADR-0010What lands
docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md— new MADR-formatted ADR with the four sub-choices, decision drivers, considered options, consequences, confirmation criteria, open production-hardening question, and the related-ADRs map.docs/decisions/README.md— one new index row for ADR-0024 (proposed, tagsbackend, security, observability, 2026-05-19).No source-code changes. No
CLAUDE.mdupdate — the ADR stays inproposeduntil reviewed, so the accepted-ADRs roll-up at the top ofCLAUDE.mdstays at 0001 → 0023. Promotion toacceptedlands in the same PR that ships the first implementation chantier (proto vendor +AiClientModule), at which pointCLAUDE.mdgets the "0024 accepted" line.Notes for the reviewer
roles[]) is a separate proposed ADR; the ingestion-through-BFF path waits for the admin app's "manage AI corpus" surface; tool dispatch is wired but exercised against an empty registry in v1.HashUserIdServicesalt has to land in both repos' deployment config soapf-ai-service.audit_log.actor_id_hashandapf_portal.audit.events.actor_id_hashproduce identical values. Recorded as an open item in the ADR's "More Information" section; the deployment doc that distributes the secret is a v1-launch deliverable.apf-ai-servicecross-reference. The ADR referencesapf-ai-service/docs/adr/ADR-0010(POC unsigned principal) andapf-ai-service/docs/adr/ADR-0011(mono-transport gRPC) as upstream anchors. Both are already accepted on the AI side. The "production hardening" decision will be a coordinated amendment in both repos on the same date.DownstreamApiClient(ADR-0014) reuse. The OBO pattern in ADR-0014 targets Entra-protected downstreams that validate the user's access token.apf-ai-serviceis not Entra-protected — it accepts an unsigned Principal proto. The ADR explicitly calls this out so the reader does not expect symmetry with the Entra-protected downstream path.Test plan
pnpm run --silent prettier --check docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md— passes (hook ran on commit).0005,0009,0010,0012,0013,0014,0017, plusCLAUDE.md).docs/decisions/README.mdfollows the table's existing format (column count, tag vocabulary, date format).backend,security,observabilityare all in the existing vocab.What's next (once accepted)
AiClientModuleskeleton — vendors the protos, wirests-protocodegen, sets up the NestJS module with the metadata interceptor and the Principal mapper, all tested against an in-process fake gRPC server. No live endpoint yet.ai-bridgecontroller —POST /api/ai/chat(SSE),GET /api/ai/rag/search,GET /api/ai/models, live againstapf-ai-servicein the dev Compose stack.portal-shellconsuming the SSE endpoint.Proposes the integration contract between portal-bff and apf-ai-service: - BFF dials the AI service over native gRPC HTTP/2 (h2c in dev, h2 + TLS in prod) via @grpc/grpc-js + ts-proto stubs. - Browser-facing chat surface is text/event-stream — each ChatEvent oneof case translates to one SSE frame; cancellation flows browser-close → call.cancel() → upstream LLM stop. - Proto files vendored from apf-ai-service/contract/proto into the BFF with on-demand codegen; both proto + generated stubs committed for hermetic builds and reviewable diffs. - POC ships unsigned Principal { subject, roles[], attributes{} } in the proto body, mirroring apf-ai-service's ADR-0010. Subject is the Entra oid hashed with the same HashUserIdService salt as the audit module so apf_portal and apf-ai-service audit trails join cleanly. - Production hardening choice (signed envelope vs mTLS) deferred until the first production deployment is in scope; recorded as an open item to be amended jointly in both repos.