Most messaging systems treat communication channels as separate products.
Web chat creates one conversation.
SMS creates another.
Email creates a third.
From a user's perspective, however, there is only one conversation. They simply continue talking through whatever channel is most convenient.
That observation shaped one of the core architectural decisions behind our messaging platform: a conversation should exist independently of the transport carrying its messages.
The Problem
Initially, every visitor communicated through an embedded web chat.
That worked well while the browser remained open.
The moment someone closed the tab or left their computer, the conversation effectively stopped. Agents lost the ability to continue communicating even though the customer had already established contact.
The obvious solution was allowing visitors to continue the conversation over SMS.
The architectural challenge was deciding what "continue" actually meant.
Should SMS create a brand new conversation?
Or should it become another transport for the existing one?
Creating another conversation introduced several problems immediately.
- Agents would lose historical context.
- Customers would appear multiple times in the inbox.
- Conversation analytics would become fragmented.
- Future features would require synchronizing multiple timelines.
Instead, we chose a different model.
A conversation represents communication between two parties. Whether messages arrive through a browser or a phone number becomes an implementation detail rather than the identity of the conversation itself.
Separating Domain from Transport
One useful way to think about the architecture is to separate what happened from how it arrived.
Instead of modeling Web Chat and SMS as different conversation types, both become message transports feeding the same domain model.
Once messages enter the system, they follow the same lifecycle regardless of origin.
The persistence layer, agent interface, notification logic, and conversation history remain unchanged.
Only the transport adapter differs.
Continuing Instead of Recreating
When a visitor chooses to continue via SMS, the platform does not create another chat.
Instead, it enriches the existing conversation with an additional communication channel.
That decision simplifies nearly every downstream system.
Agents continue working inside the same conversation.
Historical messages remain visible.
Future messages simply arrive through a different transport.
To the rest of the application, nothing fundamental has changed.
The conversation is still the same entity.
Only the delivery mechanism evolved.
Normalizing Message Flow
Supporting multiple transports often leads to duplicated business logic.
Web chat has one message pipeline.
SMS has another.
Push notifications have a third.
Over time, those implementations diverge.
Instead, incoming messages are normalized before entering the domain layer.
Regardless of whether a message originated from a browser session or an SMS webhook, the application converts it into the same internal representation.
Conceptually, the flow looks like this.
By the time business logic executes, it no longer needs to know where the message originated.
That significantly reduces conditional logic throughout the application.
Preserving Agent Context
One design goal remained constant throughout implementation.
Agents should never have to think about transports.
If an agent opens a conversation, they expect to see a complete history.
Not:
- Web Chat History
- SMS History
- Imported Messages
Just one chronological timeline.
This dramatically improves the support experience because context remains intact regardless of how customers decide to communicate.
The transport disappears behind the conversation itself.
Customers naturally switch devices and communication channels. The software should absorb that complexity rather than exposing it to support teams.
Conversation Identity
A subtle challenge emerged once multiple transports became possible.
If conversations are transport-independent, what actually identifies them?
The answer is no longer a browser session or a phone number.
Those values may change throughout the conversation's lifetime.
Instead, transport-specific identifiers become metadata attached to the conversation rather than the conversation's identity itself.
That distinction allows additional transports to be introduced later without redesigning the underlying model.
Today that might be SMS.
Tomorrow it could be WhatsApp or another messaging platform.
The domain model remains stable.
Reducing Architectural Coupling
One unexpected benefit of this design was reduced coupling between infrastructure and business logic.
Transport adapters become responsible for translating external events into internal messages.
Once translation is complete, every downstream service behaves identically.
That separation keeps business rules focused on conversations rather than communication protocols.
As additional transports are introduced, they primarily require new adapters instead of changes throughout the application.
Lessons Learned
Initially it was tempting to think in terms of communication channels.
Web Chat.
SMS.
Future messaging platforms.
But those are infrastructure concerns.
The business problem is much simpler.
Two people are having a conversation.
Everything else exists to deliver messages between them.
Once that distinction became clear, many implementation decisions naturally followed.
Conversation storage became simpler.
Agent workflows became simpler.
Future extensibility became simpler.
The architecture improved not because it supported another transport, but because it stopped making transports part of the core domain.
Future Improvements
The current implementation supports browser chat and SMS.
The architecture naturally lends itself to additional transports without significant changes to the conversation model.
Future work could include:
- Additional messaging providers
- Rich messaging channels
- Voice conversation metadata
- Unified delivery status tracking
- Transport capability negotiation
The underlying domain model would remain unchanged.
Only new transport adapters would be introduced.
One of the most useful lessons from this implementation was recognizing that communication channels are temporary, but conversations are not.
Modeling the conversation as the primary entity allowed transport-specific concerns to remain isolated at the edges of the system, resulting in an architecture that is easier to extend, easier to reason about, and significantly more pleasant for both engineers and support teams.