In-app Chat (Grimoire) - #577
Open
skarif2 wants to merge 9 commits into
Open
Conversation
Phase 1 of in-app donor/seeker chat. Adds ChatChannelDTO/ChatMembershipDTO/ChatMessageDTO/ChatConnectionDTO and CHAT_MESSAGE notification type; the four single-table DDB models (channel keyed per request for per-request lock, per-participant membership items for channel listing, time-ordered messages with 90-day TTL, connections keyed by connectionId with GSI1 on userId); and the ChatRepository + ChatConnectionRepository ports. Model unit tests cover all four access patterns.
ChatService (AWS-free) with openChannel/sendMessage/listChannels/markRead/getHistory/lock; participant + locked + rate-limit rejection. Atomic per-channel rate limiter via a conditional DynamoDB counter. Composite Chat/ChatConnection DynamoDB adapters implementing the Phase 1 ports, plus targeted lastMessageAt bumps. channelId helpers moved to commons so the application layer stays adapter-free.
WebSocket connect/disconnect/sendMessage handlers plus a Cognito JWT $connect authorizer (aws-jwt-verify). sendMessage resolves the sender from the connection store, enforces participant-only/not-locked/rate-limit via ChatService, fans out to both participants' live connections, prunes stale ones on 410, and enqueues a CHAT_MESSAGE push for offline recipients. EventBridge pipe handlers open the channel (snapshotting request context from the donation post) on accept and lock it on ignore. BloodDonationService locks channels on complete (per-donor), cancel and expire. REST getHistory/listChannels/markRead handlers, participant-only. Adds @aws-sdk/client-apigatewaymanagementapi and aws-jwt-verify.
Add GET /chat/channels, POST /chat/history and PATCH /chat/read paths, schemas and Lambda integrations, and register the three REST chat lambdas (filtered lambda_metadata) so the existing API-GW invoke permission and OpenAPI ARN substitution cover them.
WebSocket client with token-querystring auth and exponential-backoff reconnection; useChatInbox (client-side unread from lastMessageAt vs lastReadAt, optimistic markRead with rollback) and useChatRoom (REST history, live fanout, offline send-queue flushed on reconnect, markRead-on-open); CHAT_MESSAGE notification routes to a new ChatRoom screen. WEBSOCKET_URL is an optional Expo config var so chat-gated builds still succeed.
ChatInbox and ChatRoom screens with context header and read-only locked banner, gated behind EXPO_PUBLIC_CHAT_ENABLED, with Chat entry buttons on the seeker donor list and donor responses. getHistory now returns the channel status+context snapshot alongside the message page so the room header renders on a cold-start deep-link without a separate post fetch.
Resolve the three tf-security (Checkov) findings on the chat WebSocket infra so `make tf-security` passes: - CKV_AWS_76: enable stage access logging via a dedicated CloudWatch log group + access_log_settings (mirrors the REST api_gw pattern) - CKV_AWS_309: suppress on the $disconnect and sendMessage routes; the connection is authorized once at $connect via the Cognito custom authorizer and WebSocket routes do not re-authorize per message - CKV2_AWS_51: suppress on the stage; client-cert/mTLS applies to HTTP backends, but the chat integrations are Lambda AWS_PROXY Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsNmuBjgsa4aBfKg6AmbWE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds real-time, authenticated, participant-only in-app chat between a donor and a seeker, full-stack (backend business logic, AWS infrastructure, and the Expo mobile client). A chat channel opens automatically when a donor accepts a blood request (and re-opens on re-accept), locks when the request resolves (IGNORED, COMPLETED, CANCELLED, or EXPIRED), and purges messages after 90 days. Users get live delivery over WebSocket with an offline push-notification fallback, a channel inbox with an unread indicator, and paginated newest-first history. The whole feature is gated behind the
EXPO_PUBLIC_CHAT_ENABLEDbuild-time flag so it can be shipped dark and rolled out deliberately.The work was delivered in seven dependency-ordered phases (domain model → service layer → infrastructure → handlers → OpenAPI/REST wiring → mobile data layer → mobile UI).
Fixes / Resolves
Type of change
What changed
Domain model & DTOs (
commons/)commons/dto/ChatDTO.ts:ChatChannelDTO,ChatMembershipDTO,ChatMessageDTO,ChatConnectionDTO, theChatContextSnapshot,ChatChannelStatus/ChatRoleenums, and the sharedbuildChannelId/parseChannelId/isChannelParticipanthelpers (one source of truth for the compositechannelId = "seekerId#requestPostId#donorId").CHAT_MESSAGEto theNotificationTypeenum.Business logic (
core/application/chatWorkflow/)ChatService(AWS-free):openChannel,sendMessage,listChannels,markRead,getHistory,lockChannel,lockChannelsForRequest.channelId(no DB read); send path enforces not-locked + an atomic DynamoDB rate limit (60 msg/min/channel).BloodDonationServicehook: locks channels on COMPLETED (per donor) and CANCELLED/EXPIRED (per request), via an optional injectedChatService(existing call sites unaffected).Storage (single DynamoDB table)
PK=CHANNEL#<seekerId>#<requestPostId>,SK=DONOR#<donorId>) with status + a request-context snapshot.PK=CHATUSER#<userId>) so each user lists their own channels without a shared GSI.connectionIdwithGSI1onuserIdfor fanout.Infrastructure (
iac/terraform/aws/chat/)$connect/$disconnect/sendMessageroutes), a REQUEST Lambda authorizer on$connect, explicit deployment + stage, the six WS/pipe Lambdas and three REST Lambdas, and IAM.ACCEPTED#) → channel creator, REMOVE(ACCEPTED#) → channel locker.ttlattribute.Lambda handlers (
core/services/aws/chat/)chatConnect/chatDisconnect,chatConnectAuthorizer(verifies the Cognito access token withaws-jwt-verify),chatChannelCreator/chatChannelLocker(pipe handlers),chatSendMessage(resolve sender by connection, persist, fan out viaApiGatewayManagementApi, prune stale connections, enqueue offline push), and thechatGetHistory/chatListChannels/chatMarkReadREST handlers.REST API (
openapi/)bloodconnect-chatendpoints:GET /chat/channels,POST /chat/history,PATCH /chat/read(POST/PATCH with JSON bodies so the compositechannelIdand the opaque pagination cursor round-trip cleanly), wired to their Lambdas viax-amazon-apigateway-integrationand VTL templates.Mobile (
clients/mobile/src/chat/)ChatWebSocketClient(exponential-backoff reconnect, offline send queue),chatService(REST), and theuseChatInbox/useChatRoomhooks. Unread is derived client-side.ChatInbox(channel list + unread badge),ChatRoom(sent/received bubbles, context header, read-only locked banner), andChatRoomHeader.CHAT_MESSAGEpush deep-links to the room.isChatEnabled()(EXPO_PUBLIC_CHAT_ENABLED) gates both the navigation routes and the entry-point buttons.Notable design decisions
getHistoryreturns{ channel: {status, context}, page }, not just messages, so the chat room header and locked banner render on a cold-start deep-link from the one call the room already makes — no separate post fetch. (The context was snapshotted onto the channel at creation specifically for this path.)$connectheaders.Checklist
Dependencies
Testing & verification
TZ=UTC npm test→ 52 suites / 384 tests pass (the 2formattedDatecases are local-timezone artifacts that pass underTZ=UTC/CI).ChatService, DDB adapters, pipe/REST/WS handlers, and theBloodDonationServicelock hooks are unit-tested (aws-sdk-client-mock).CHAT_MESSAGErouting, and render tests forChatInbox/ChatRoom(header on deep-link, open vs locked) plus the entry-point and rollout-flag gating.Toast/FlatListtypings at shifted line numbers).terraform validate+fmt -checkpass per-module for thechatanddynamodbmodules;make lint-apiis clean. Roottf-validateis deferred to CI (a pre-existingtemplate_fileprovider limitation on darwin_arm64).Notes
EXPO_PUBLIC_CHAT_ENABLEDunset, no chat routes or entry points are exposed and a build withoutWEBSOCKET_URLstill succeeds.listChannelshas no pagination cursor and membership items have no TTL, so a very prolific user's channel partition could eventually truncate at the 1 MB query page; mitigation is to add a cursor mirroringgetHistoryif it ever bites.Cost Breakdown (Claude Generated)
Feature total: ≈ $255 · Active work: ≈ 4.25 h · spread across a 14.2 h window (10:40 AM → 00:52 AM) because of the two ~5-hour limit resets you waited through.