Communication
Status: Work in progress
| Version | Date | Author | Status | Description |
|---|---|---|---|---|
| 0.1 | 2025-09-09 | isierra | WIP | Initial version |
Introduction
The Communication service is responsible for sending emails, SMS, and other types of notifications. It includes functionality to send messages to users, groups, and other services. Additionally, it manages long-running conversations between users and services.
Main Concepts
- Contact Type: Defines a specific way of contacting or sending information to a recipient (e.g., email, SMS, webhook). Specifies the schema for the contact data required for each type.
- Channel Type: The implementation of a message-sending medium (e.g., SMTP, Twilio, MSGraph for email; ACS, Twilio for SMS). Defines how messages are sent and what configuration is required.
- Channel: A configured instance of a channel type, set up with specific settings to send messages (e.g., a particular SMTP server, a Twilio account, etc.).
- Message: The content that is sent through a channel to a collection of destinations.
- MessageThread: A long-running interaction between users and services, which may involve multiple messages and responses.
Contact Type Requirements
A contact type defines a specific way of contacting or sending information to a recipient, like email, SMS, etc. Each contact type has the following properties:
| Field | Type | Description |
|---|---|---|
Id | constant string | Unique identifier for the contact type. |
DisplayName | string | Human-readable name for the contact type. |
Description | string | Description of the contact type. |
ContactSchema | JSON Schema | Schema for the contact data required for this contact type. |
Each contact in the system will have an associated contact type, which will determine how the contact information is structured and validated.
List Contact Types
There must be a public API endpoint to list all available contact types.
| Field | Filter | Sort by |
|---|---|---|
Id | ||
DisplayName | ✅ | ✅ default |
Description | ✅ |
Get Contact Type Details
There must be a public API endpoint to get the details of a specific contact type(s) by its Id / Ids. All fields must be returned.
Example Contact Types
Each contact type must have an associated permission, indicating whether a user can create contacts of that type for himself or for other users.
Email Contact Type
Contact via email address.
-
Id:cntp_email -
DisplayName:Email -
Description:Contact via email address. -
ContactSchema:{"type": "object","properties": {"email": {"type": "string","format": "email","description": "Recipient email address"}},"required": ["email"]}
Phone Contact Type
Contact via phone number, typically for SMS, voice calls, etc.
-
Id:cntp_phone -
DisplayName:Phone -
Description:Contact via phone number. -
ContactSchema:{"type": "object","properties": {"phoneNumber": {"type": "string","pattern": "^\\+?[1-9]\\d{1,14}$","description": "Recipient phone number in E.164 format"}},"required": ["phoneNumber"]}
Tdp User Contact Type
Contact via internal user identifier.
-
Id:cntp_user -
DisplayName:Tdp User -
Description:Contact via internal user identifier. -
ContactSchema:{"type": "null"}
This type of contact does not require any additional information, as the system will use the UserId to look up the user’s contact information internally.
Webhook Contact Type
Contact via a webhook URL.
-
Id:cntp_webhook -
DisplayName:Webhook -
Description:Contact via a webhook URL. -
ContactSchema:{"type": "object","properties": {"url": {"type": "string","format": "uri","description": "Webhook URL"},"httpMethod": {"type": "string","enum": ["POST", "GET", "PUT", "DELETE", "PATCH"],"default": "POST","description": "HTTP method to use for the webhook"},"headers": {"type": "object","additionalProperties": { "type": "string" },"description": "Custom headers to include in the webhook request, like Authorization"},"route": {"type": "object","additionalProperties": { "type": "string" },"description": "Route parameters to include in the webhook URL. Use {param} in the URL to indicate a route parameter."},"query": {"type": "object","additionalProperties": { "type": "string" },"description": "Query parameters to include in the webhook URL."},"format": {"type": "string","enum": ["multipart", "json", "xml", "summary"],"description": "Format of the message payload"}},"required": ["url", "httpMethod"]}
Other Contact Types
Other contact types can be defined similarly, depending on client’s needs, such as:
Channel Type Requirements
A channel type defines a specific implementation of message sending medium, like SMTP, IMAP, MSGraph for email, ACS, Twilio for SMS, etc. Each channel type has the following properties:
| Field | Type | Description |
|---|---|---|
Id | constant string | Unique identifier for the channel type. |
ContactTypeId | string | Identifier of the associated contact type. |
DisplayName | string | Human-readable name for the channel type. |
Description | string | Description of the channel type. |
ConfigSchema | JSON Schema | Schema for the configuration data required to set up the channel. |
CreationMode | CreationMode enum | Specifies how the channels should be created. |
The ContactTypeId must refer to a valid contact type defined in the system, and indicates the type of contact information that will be used with this channel type.
The CreationMode enum has the following possible values:
| Value | Description |
|---|---|
BuiltIn | Channels of this type are built-in and cannot be created or modified by users. |
Manual | Channels of this type must be created manually by users. |
List Channel Types
There must be a public API endpoint to list all available channel types.
| Field | Filter | Sort by |
|---|---|---|
Id | ✅ | |
DisplayName | ✅ | ✅ default |
ContactTypeId | ✅ |
Get Channel Type Details
There must be a public API endpoint to get the details of a specific channel type(s) by its Id / Ids.
All fields must be returned.
Example Channel Types
Email SMTP
Using MailKit nuget package, we can send emails using SMTP protocol.
-
Id:chtp_smtp -
DisplayName:Email SMTP -
Description:Send emails using SMTP protocol. -
CreationMode:Manual -
ContactTypeId:cntp_email -
ConfigSchema:{"type": "object","properties": {"host": {"type": "string","description": "SMTP server hostname or IP address","minLength": 1,"maxLength": 255,"pattern": "^(([a-zA-Z0-9]+)(\\-([a-zA-Z0-9]+))*)(\\.(([a-zA-Z0-9]+)(\\-([a-zA-Z0-9]+))*))*"},"port": {"type": "integer","description": "Port number between 1 and 65535","minimum": 1,"maximum": 65535},"security": {"type": "string","enum": ["None", "Auto", "TLSOnConnect", "StartTLS", "StartTLSIfAvailable"],"default": "Auto","description": "Security protocol to use"},"authentication": {"type": "string","enum": ["Anonymous", "Login", "Plain", "OAuth2", "OAuthBearer", "CramMd5", "DigestMd5", "Ntlm", "ScramSha1", "ScramSha256", "ScramSha512"],"default": "Auto","description": "Authentication method to use"},"username": {"type": "string","description": "SMTP server username"},"password": {"type": "string","description": "SMTP server password"},"authToken": {"type": "string","description": "OAuth2 authentication token"}},"required": ["host", "port", "security", "authentication"]}
SMS Azure Communication Services
Using Azure.Communication.Sms nuget package, we can send SMS using Azure Communication Services.
-
Id:chtp_sms_azure -
DisplayName:SMS Azure Communication Services -
Description:Send SMS using Azure Communication Services. -
CreationMode:Manual -
ContactTypeId:cntp_phone -
ConfigSchema:{"type": "object","properties": {"credentials": {"type": "string","enum": ["default", "connectionString"],"description": "Azure Communication Services credentials type","default": "default"},"endpoint": {"type": "string","description": "Azure Communication Services endpoint URL. Required if credentials is 'default'"},"connectionString": {"type": "string","description": "Azure Communication Services connection string. Required if credentials is 'connectionString'"}},"required": ["credentials"]}
Tdp Internal Messaging
Using internal messaging system to send in-app notifications.
Id:chtp_tdp_internalDisplayName:Tdp Internal MessagingDescription:Send in-app notifications using Tdp internal messaging system.CreationMode:BuiltInContactTypeId:cntp_userConfigSchema:{ "type": "null" }
Webhook Channel Type
Using webhooks to send messages to external services.
Id:chtp_webhookDisplayName:WebhookDescription:Send messages to external services using webhooks.CreationMode:ManualContactTypeId:cntp_webhookConfigSchema:{ "type": "null" }
Other Social Media Channel Types
Other social media channel types can be defined similarly, depending on client’s needs, such as:
Channel Type Behavior
Each channel type must implement a subscription to Message creation events. When a new Message is created, the channel type must check if it can handle the message based on the ChannelTypeId of the associated Contacts.
Channel Requirements
A channel represents a specific instance of a channel type, configured with the necessary settings to send messages. Each channel has the following properties:
| Field | Type | Description |
|---|---|---|
Id | string | Unique identifier for the channel. |
TenantId? | string | Identifier of the tenant that owns the channel. |
SharingMode | ChannelSharingMode | Specifies how the channel is shared. |
DisplayName | string | Human-readable name for the channel. |
Description | string | Description of the channel. |
ChannelTypeId | string | Identifier of the channel type. |
ContactTypeId | string | Identifier of the contact type. |
Config | string | Encrypted configuration data for the channel. |
From? | Contact JSON | The optional default sender for messages sent through this channel. |
FromUserId? | string | Optional identifier of the user associated with the default sender. |
Priority | int | Priority of the channel type with respect to it’s contact type. |
Each channel can be associated with a project (tenant), allowing for project-specific channels.
The ChannelSharingMode enum has the following possible values:
Private: The channel is only accessible by the owner tenant.Shared: The channel can be accessed by other tenants, but only the owner tenant can modify it. Only applies whenTenantIdis not set (global channel).
The From field is optional, but if provided, it must conform to the ContactSchema defined by the associated contact type. The messages sent through this channel will use this default sender unless overridden in the message. The message’s From field will be updated to the channel’s default sender if not provided. The same applies to the FromUserId field, which in this case represents the user associated with the default sender. The message’s FromUserId field will be updated to the channel’s FromUserId only if the message’s From field is not provided.
The Priority field is used to determine the order in which channel are considered when multiple channels share the same contact type. A higher value indicates a higher priority. When sending a message to a given destination, all channels with types that match the contact type of the destination will be considered. All non-shared channels have more priority than shared channels. Among channels with the same sharing mode, channels with higher priority values will be preferred. If multiple channels have the same priority, the system may choose any of them at random.
Create Channel
To create a channel, all fields except Id must be provided. The Id will be generated by the system with prefix chnl followed by a unique identifier.
The TenantId is taken from the RequestContext. Only when the TenantId is not set (global channel), the SharingMode can be set to Shared.
When selecting a channel type, only channel types with CreationMode set to Manual can be selected.
The Config field must conform to the ConfigSchema defined by the selected channel type. The configuration data must be encrypted before storing it in the database.
Update Channel
To update a channel, all fields except Id, ChannelTypeId and TenantId can be updated.
See the notes on Create Channel regarding TenantId, SharingMode, and Config fields.
Delete Channel
To delete a channel, the Id of the channel must be provided. Only channels whose channel type has CreationMode set to Manual can be deleted.
List Channels
There must be a public API endpoint to list all channels. The following filters and sorting options must be supported:
| Field | Filter | Sort by |
|---|---|---|
Id | ✅ | |
DisplayName | ✅ | ✅ default |
ChannelTypeId | ✅ | |
SharingMode | ✅ |
Get Channel Details
There must be a public API endpoint to get the details of a specific channel(s) by its Id / Ids.
All fields must be returned, except the TenantId and Config fields, which are only returned if the RequestContext has the same TenantId as the channel, or if the channel is global (TenantId is null) and its SharingMode is Shared.
Additional fields are included in the response:
| Field | Type | Description |
|---|---|---|
CanUpdate | bool | Indicates if the channel can be modified by the requester. |
CanDelete | bool | Indicates if the channel can be deleted by the requester. |
Message Requirements
A message represents the content that is sent through a channel to a collection of destinations. The message model follows an approximation of the RFC 5322 standard for email messages, but keeping only the relevant fields for our use case.
| Field | Type | Description |
|---|---|---|
Id | string | Unique identifier for the message. |
TenantId? | string | Identifier of the tenant that owns the message, if applicable. |
Subject | string | Subject of the message. |
TextContent? | string | Plain text content of the message. |
HtmlContent? | string | HTML content of the message. |
Resources | ResourceContent[] | List of inline content or attachments to be included in the message. |
Destinations | Destination[] | List of destinations to which the message will be sent. |
NotificationType | ModuleElementId | Type of notification the message is associated with. |
MessageThreadId? | string | Optional identifier of the message thread the message belongs to. |
CreatedAt | DateTime | Timestamp when the message was created. |
The TenantId is optional, as messages can be global (not associated with any tenant). If set, it must be taken from the RequestContext.
The Subject field must be a non-empty string with a maximum length of 255 characters.
The TextContent and HtmlContent fields are optional, but at least one of them must be provided. If both are provided, the HtmlContent will be used as the primary content, and the TextContent will be used as a fallback for clients that do not support HTML.
The Resources field is a list of inline content or attachments to be included in the message. Each resource must have a unique Id within the message, but not globally. See the Resource Content Model section for more details.
The Destinations field is a list of destinations to which the message will be sent. See the Destination Model section for more details.
The NotificationType field is used to categorize the message. It must refer to a valid ModuleElementId that represents a notification type defined in the system.
The MessageThreadId field is optional and can be used to associate the message with a long-running conversation or thread.
The CreatedAt field must be set to the current timestamp when the message is created.
Resource Content Model
A resource content represents a piece of content that is embedded within the message body, such as images or other media, or attached as files. Each resource content has the following properties:
| Field | Type | Description |
|---|---|---|
Id | string | Unique identifier for the content. |
ContentType | string | MIME type of the inline content. |
Disposition | ResourceContentDisposition | Indicates if the content is inline or an attachment. |
FileName | string | File name of the inline content. |
ContentFormat | MessageContentFormat | Format of the Content field. |
Content | byte[] | Content data. |
If inline, the Id of the content must be referenced in the HTML content using the cid: URI scheme, for example: <img src="cid:content-id">.
The ContentType must be a valid MIME type, such as image/png or image/jpeg, video/mp4, application/pdf, etc.
The Disposition enum has the following possible values:
Inline: The content is embedded within the message body and referenced in the HTML content.Attachment: The content is sent as an attachment to the message.
The FileName is optional if inline, but required if the content is an attachment.
The MessageContentFormat enum has the following possible values:
Binary: The whole content is included as a binary array.FileId: The content is referenced by a file identifier, which can be used to retrieve the content from a file storage service. In this case, theContentfield contains the file identifier as a UTF-8 encoded string.
Destination Model
A destination represents a recipient of the message. Each destination has the following properties:
| Field | Type | Description |
|---|---|---|
Id | string | Locally unique identifier for the destination within the message. |
ContactTypeId | string | Identifier of the contact type for the destination. |
From? | Contact JSON | The optional sender of the message. |
FromUserId? | string | Optional identifier of the user associated with the sender contact. |
SentDate? | DateTime | Timestamp when the message was sent by the channel. |
SentByChannelId? | string | Identifier of the channel that sent the message. |
Contact | Contact JSON | Contact information for the recipient. |
DisplayName? | string | Optional human-readable name for the destination. |
UserId? | string | Optional identifier of the user associated with the contact. |
Grouping | DestinationGrouping | Specifies how the message should be grouped. |
When the From and FromUserId fields are not provided, the default sender defined in the channel configuration must be used during message creation.
The DestinationGrouping enum has the following possible values:
Target: If the channel type supports it, messages to multiple destinations can be merged into a single message (e.g., To in emails).Copy: If the channel type supports it, messages to multiple destinations can be merged, and all recipients are visible to each other (e.g., CC in emails).Protected: If the channel type supports it, messages to multiple destinations can be merged, but recipients are hidden from each other (e.g., BCC in emails).Private: Messages are sent individually to each destination, without merging.
The SentDate and SentByChannelId fields are automatically set by the system when the message is successfully sent by the channel for this particular destination.
Message User Status
A message user status represents the read/unread status of a message for a specific user. Each message user status has the following properties:
| Field | Type | Description |
|---|---|---|
MessageId | string | Identifier of the message. |
UserId | string | Identifier of the user. |
ReadAt? | DateTime | Timestamp when the message was read by the user. |
ArchivedAt? | DateTime | Timestamp when the message was archived by the user. |
Each time a message is sent to a destination associated with a user (i.e., the UserId field is set), a message user status must be created with the ReadAt field set to null.
When a user reads a message, the ReadAt field must be updated to the current timestamp.
The event MessageWasReadByUser must be published in the stream of the message, and it must be projected into the MessageThread’s Participants and the MessageUserStatus read models. A user could issue a MessageWasUnreadByUser event to mark a message as unread, setting the ReadAt field back to null.
When a user archives a message, the ArchivedAt field must be updated to the current timestamp. A user could issue a MessageWasUnArchivedByUser event to un-archive a message, setting the ArchivedAt field back to null.
Create Message
To create a message, all fields except Id and CreatedAt must be provided. The Id will be generated by the system with prefix msg followed by a unique identifier. The CreatedAt field will be set to the current timestamp.
Confirm Message Sent
The system will issue an update to the destination’s SentDate, From, and FromUserId fields when the message is successfully sent by the channel for a particular destination.
This command must be idempotent, as the channel may retry sending the message in case of failures.
The command can issue the update of multiple destinations at once, but only for destinations that belong to the same message.
List Messages
There must be a public API endpoint to list all messages. The following filters and sorting options must be supported:
| Field | Filter | Sort by |
|---|---|---|
Id | ✅ | |
UserMode | ✅ | |
Subject | ✅ | |
NotificationType | ✅ | |
MessageThreadId | ✅ | |
CreatedAt | ✅ | ✅ |
SentDate | ✅ | ✅ default descending |
ReadAt | ✅ | ✅ |
ArchivedAt | ✅ | ✅ |
The filter UserMode has the following possible values, and is required, unlike every other filter:
ReceivedByMe(default): Messages sent to destinations associated with the current user. The fieldUserIdof at least one destination of the message must be set to the current user’s identifier.SentByMe: Messages sent to destinations associated with the current user. The fieldFromUserIdof the message must be set to the current user’s identifier.
Get Message Details
There must be a public API endpoint to get the details of a specific message(s) by its Id / Ids.
All fields must be returned, except the TenantId field.
Message Summarizers Requirements
There different kinds of summarizers in this service, that allow converting a message model into a different format that can be sent through a specific kind of channel.
Each channel type can decide how to use these summarizers, depending on its capabilities and requirements.
Text Summarizer
The text summarizer condenses the message content into a shorter form, preserving the main ideas and context. This is useful for channels with strict character limits or for generating previews, like SMS, push notifications, X, WhatsApp, or in-app notifications, etc. This summarizer receives a Message object and an maximum length, and returns a plain text summary.
HTML Summarizer
The HTML summarizer converts the message content into a simplified HTML format, suitable for channels that support rich text but have limitations on complexity or size. This is useful for generating email previews or messages for platforms that support basic HTML formatting. This summarizer receives a Message object and returns a simplified HTML string.
URL Summarizer
The URL summarizer generates a short URL that links to the full message content hosted on a web service. This is useful for channels with very limited space, such as SMS or social media posts. The short URL can be included in the message to allow recipients to access the full content. This summarizer receives a Message object and returns a short URL string.
This summarizer would require access to the base URL of the web service where the full message content is hosted.
Message Aggregator
The message aggregator combines multiple messages into a single message, preserving the context and relationships between them. This is useful for generating digests, like weekly summaries of notifications. This summarizer receives a list of Message objects and returns a single aggregated Message object.
Message Thread Requirements
A message thread represents a long-running interaction between users and services, which may involve multiple messages and responses. Each message thread has the following properties:
| Field | Type | Description |
|---|---|---|
Id | string | Unique identifier for the message thread. |
TenantId? | string | Identifier of the tenant that owns the message thread, if applicable. |
EntityType? | ModuleElementId | Optional type of the entity the thread is associated with. |
EntityId? | string | Optional identifier of the entity the thread is associated with. |
Caption | string | Human-readable caption for the message thread. |
CreatedAt | DateTime | Timestamp when the message thread was created. |
ClosedAt? | DateTime | Timestamp when the message thread was closed. |
StartDate? | DateTime | Timestamp when the first message was sent in the thread. |
LastDate? | DateTime | Timestamp when the last message was sent in the thread. |
Participants | Participant[] | List of participants in the message thread. |
MessageCount | int | Total number of messages in the thread. |
The TenantId is optional, as message threads can be global (not associated with any tenant). If set, it must be taken from the RequestContext.
A message thread can be created about any particular subject, represented by the EntityType and EntityId fields. These fields are optional, as a message thread may not be associated with any specific entity. Usually, they are created by the system when a user of the system sends a message from a particular UI related to a given entity, like a work order, a zone, etc. It could be useful to allow a user start new message threads from a generic UI, not related to any particular entity, to discuss general topics with other users, if a client requires it. The entities that allow built-in association with message threads must add a MessageThreadId? field to their model, and set up the UI to allow users to create and view message threads associated with that entity.
The Caption field must be a non-empty string with a maximum length of 255 characters. It usually is created from a given entity, like a work order “Clean the filters of the AC unit”, so the caption could be the same, or based on it.
The dates CreatedAt and ClosedAt are automatically set by the system when the message thread is created and closed, respectively.
The StartDate and LastDate fields are automatically updated by the system when messages are added to the thread. The StartDate is set to the timestamp of the first message, and the LastDate is updated to the timestamp of the most recent message.
The Participants field is a list of participants in the message thread. See the Participant Model section for more details.
The MessageCount field is automatically updated by the system each time a new message is added to the thread. It could be updated only in the projection, without the need to issue a command to update it.
Participant Model
A participant represents a user involved in the message thread. Each participant has the following properties:
| Field | Type | Description |
|---|---|---|
MessageThreadId | string | Identifier of the message thread. |
UserId | string | Identifier of the user. |
UnreadMessageCount | int | Number of unread messages for the user in the thread. |
The participant model could be implemented as a separate read model, projected from the MessageWasSentToUser and the MessageWasReadByUser events in the message stream.
Create Message Thread
To create a message thread, all fields except Id, CreatedAt, ClosedAt, StartDate, LastDate, and MessageCount must be provided. The Id will be generated by the system with prefix mthd followed by a unique identifier. The CreatedAt field will be set to the current timestamp. The ClosedAt, StartDate, and LastDate fields will be set to null, and the MessageCount field will be initialized to zero.
Update Descriptions on Message Thread
The message thread gets updated as a projection of events from the message stream. The only field that can be updated directly is the Caption field.
Close Message Thread
To close a message thread, the Id of the message thread must be provided. The ClosedAt field will be set to the current timestamp. Once a message thread is closed, no new messages can be added to it.
Each entity responsible for creating message threads must choose when to close them, based on their own business logic. For example, a work order could close its associated message thread when the work order is completed or cancelled. A reopening of the work order could create a new message thread, or reopen the existing one, based on the client’s requirements. In such case, a reopening of message threads should be implemented as well.
List Message Threads
There must be a public API endpoint to list all message threads. The following filters and sorting options must be supported:
| Field | Filter | Sort by |
|---|---|---|
Id | ✅ | |
Caption | ✅ | ✅ |
EntityType | ✅ | |
EntityId | ✅ | ✅ |
CreatedAt | ✅ | ✅ |
ClosedAt | ✅ | ✅ |
StartDate | ✅ | ✅ |
LastDate | ✅ | ✅ default |
MessageCount | ✅ | ✅ |
UserId | ✅ |
Get Message Thread Details
There must be a public API endpoint to get the details of a specific message thread(s) by its Id / Ids.
All fields must be returned, except the TenantId field.
List Messages in Thread
There must be a public API endpoint to list all messages in a specific message thread by its Id. The following filters and sorting options must be supported:
| Field | Filter | Sort by |
|---|---|---|
MessageThreadId | ✅ required 1 | |
UserId | ✅ | |
CreatedAt | ✅ | ✅ |
UpdatedAt | ✅ | ✅ |
Content | ✅ text and html |