Skip to content
Back to Blog
messaginghealthcaretypescriptsystem-designkiosk

Messages Need a Protocol Before They Need a Chat UI

Daniel Anthony Romitelli Jr. · July 31, 2026

A patient taps a kiosk and asks for help. A staff member replies from another screen. The same message may appear in the app, trigger a push notification, fall back to Short Message Service (SMS), and later show as read.

If each screen decides what those events mean, the thread turns into a rumor mill. One client marks the message as sent. Another treats a push attempt as delivery. A third retries after a network pause. In a clinic, that ambiguity creates audit gaps, extra staff work, and a patient expectation problem: sent must mean something actionable.

The rule I wanted was simple: every message has a durable state, every transition has an owner, and urgent communication can leave the app channel when policy allows it.

That rule is why I built BidirectionalMessagingService as the coordination layer for kiosk messaging, rather than treating chat as a screen-level feature.

The invariant comes before the interface

The kiosk already had services for push, SMS, email, and templates. Those services deliver through channels. They do not decide the truth of the thread.

BidirectionalMessagingService owns the message lifecycle: creation, delivery policy, acknowledgement handling, read receipts, fallback decisions, and teardown. Push, SMS, and email remain transports.

The cost is ceremony. A basic widget can append text and look finished. This version asks callers to provide role, priority, permitted channels, and acknowledgement behavior. That extra shape buys one authority for changing the record.

The row carries the user-facing truth

The message model stores identity, direction, status, timestamps, and attachments. The durable status set is intentionally small: sent, delivered, read, and failed.

Queue membership stays in memory while work is active. It never becomes a database value. A queued item is a worker condition, not a patient-visible fact. Persisting it would force every client to explain whether the item is safe, blocked, or retrying.

Delivery options are policy, rather than a single flag:

FieldWhat it decidesCost
channelsWhich transports may carry the messageMore combinations to test
urgencyHow aggressively the system alerts usersGreater risk of alert fatigue
requireDeliveryConfirmationWhether delivery needs acknowledgementMore bookkeeping
fallbackToSMSWhether the message may leave the app channelHigher external dependency surface
translationLanguageWhether content needs language adaptationMore transformation risk

Timeouts live in the same policy layer. In my implementation, the clock starts with the active delivery attempt. If that attempt expires and no permitted route can prove delivery, the message resolves to failed. Presence can influence routing, but it does not create another stored status.

Receipts and teardown are side effects

A read action moves the message to read. Notifying the sender is a side effect of that transition. If the sender notification fails, the patient still read the message. Mixing those facts would make the history less reliable.

The same ownership applies to runtime resources. Realtime subscriptions and pending work need an explicit end, so the service owns cleanup instead of scattering it across screens. In the implementation, cleanup unsubscribes active channels, clears channel tracking, and clears the in-memory message queue. That prevents stale listeners from interpreting later kiosk activity.

That matters in a shared device flow. One patient can walk away, another can begin check-in, and an old subscription should have no vote in the new session.

The review table

Once the lifecycle is explicit, each operation has one test: it changes the durable record, triggers a side effect, or both.

EventOwnerDurable status outcomeSide effects
Staff sends messageMessaging servicesentChannel delivery attempts
Transport confirms deliveryChannel adapterdeliveredOptional confirmation notice
Patient opens messageMessaging servicereadRead receipt notification
Active attempt expiresMessaging servicefailedOptional fallback path before failure
Sender receives receipt noticeReceipt handlerNo new message statusUser interface acknowledgement

Consumer chat can tolerate fuzzy indicators. A clinic kiosk has less room for soft meaning because staff coordinate care through the thread and patients expect a reply to reach someone. With explicit states and owners, one status record survives channel retries, fallback routing, and client differences. The interface can vary; the thread still tells one story.