Skip to content

feat(messages): agency to caregiver messaging inside the platform - #192

Merged
SishirP17 merged 1 commit into
mainfrom
feat/messaging
Aug 4, 2026
Merged

feat(messages): agency to caregiver messaging inside the platform#192
SishirP17 merged 1 commit into
mainfrom
feat/messaging

Conversation

@SishirP17

Copy link
Copy Markdown
Collaborator

Why

Coordinators and caregivers were talking by personal text message, which put work conversation, and sometimes client detail, on personal phones outside any retention, audit, or BAA the agency holds.

One thread per caregiver, not per topic

A caregiver has one conversation with their office, the way a text thread works. Making somebody pick a category on a phone would just push them back to SMS. The (agency_id, caregiver_id) unique constraint makes opening a thread idempotent, so two concurrent opens cannot split a conversation in half.

Cross-agency isolation

Threads are agency-scoped, so a caregiver working at two agencies has two separate conversations and neither agency can observe the other. Staff address a caregiver by id, but the thread is always resolved inside the caller's own agency, so an id belonging to another tenant reaches a local thread that will simply never have messages, rather than the other agency's conversation. messages.agency_id is denormalized specifically so every message read is tenant-filtered without depending on a join.

Bodies are PHI

People will discuss clients here. Bodies never enter a notification payload or an audit payload. The push says only that a message arrived; the caregiver opens the app to read it.

The unread count is not a clever join

Counts are computed as aggregates. The join version has to express "newer than the read mark, or all of them when it is null", and a null comparison in SQL quietly yields no rows, which would report zero unread on a thread nobody has ever opened, the exact opposite of the truth. I wrote it as a join first and it was wrong in exactly that way.

Consistency

Posting a message and stamping the thread's activity time happen in one transaction, so a thread can never show a message it does not list or list a message without appearing active. Read state is a per-side high-water mark on the thread rather than per-message receipts, which would be a lot of write traffic for a feature nobody asked for.

The unread-count endpoint answers {count: 0} rather than failing: a badge is not worth breaking a screen over.

Migration

2026-08-04-add-messaging, two new tables. Needs applying to prod. Until then both surfaces load and every call 500s.

Tests

13 route tests: session-derived thread resolution, caregiver-vs-staff dispatch, caregiver cannot post as staff, staff cannot post as caregiver, empty/whitespace rejection, trimming, unread count degradation. All four suites pass, plus typecheck, lint, sql:scan, security:scan.

The repo's design-system guard caught a hardcoded hex I had used as a CSS fallback; it now uses the existing --color-surface-soft token.

Not included

No attachments, no read receipts, no typing indicators, and no message editing or deletion. Messages are append-only, which is the right default for a record an agency may later need to produce.

Coordinators and caregivers were talking by personal text message, which put
work conversation, and sometimes client detail, on personal phones outside
any retention, audit, or BAA the agency holds.

One thread per (agency, caregiver), not per topic. A caregiver has one
conversation with their office the way a text thread works, and making
somebody pick a category on a phone would just push them back to SMS. The
unique constraint makes opening a thread idempotent, so two concurrent opens
cannot split a conversation in half.

Threads are agency-scoped, so a caregiver working at two agencies has two
separate conversations and neither agency can observe the other. Staff
address a caregiver by id, but the thread is always resolved inside the
caller's own agency, so an id belonging to another tenant reaches a local
thread that will simply never have messages rather than the other agency's
conversation.

Message bodies are PHI: people will discuss clients here. Bodies never enter
a notification payload or an audit payload. The push says only that a message
arrived, and the caregiver opens the app to read it.

Unread counts are computed as aggregates rather than a conditional join. The
join has to express "newer than the read mark, or all of them when it is
null", and a null comparison in SQL quietly yields no rows, which would
report zero unread on a thread nobody has ever opened, the exact opposite of
the truth.

Posting a message and stamping the thread's activity time happen in one
transaction, so a thread can never show a message it does not list or list a
message without appearing active. The unread-count endpoint answers zero
rather than failing, because a badge is not worth breaking a screen over.
@SishirP17
SishirP17 requested a review from durga710 as a code owner August 4, 2026 23:34
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rayhealth-evv-platform-app Ready Ready Preview Aug 4, 2026 11:36pm

@SishirP17
SishirP17 merged commit e46a1fc into main Aug 4, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant