Skip to content

Communication

Status: Work in progress

VersionDateAuthorStatusDescription
0.12025-09-09isierraWIPInitial version

Tdp.Communications repository

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.

Diagram

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.
Diagram

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:

FieldTypeDescription
Idconstant stringUnique identifier for the contact type.
DisplayNamestringHuman-readable name for the contact type.
DescriptionstringDescription of the contact type.
ContactSchemaJSON SchemaSchema 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.

FieldFilterSort 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:

FieldTypeDescription
Idconstant stringUnique identifier for the channel type.
ContactTypeIdstringIdentifier of the associated contact type.
DisplayNamestringHuman-readable name for the channel type.
DescriptionstringDescription of the channel type.
ConfigSchemaJSON SchemaSchema for the configuration data required to set up the channel.
CreationModeCreationMode enumSpecifies 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:

ValueDescription
BuiltInChannels of this type are built-in and cannot be created or modified by users.
ManualChannels 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.

FieldFilterSort 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_internal
  • DisplayName: Tdp Internal Messaging
  • Description: Send in-app notifications using Tdp internal messaging system.
  • CreationMode: BuiltIn
  • ContactTypeId: cntp_user
  • ConfigSchema: { "type": "null" }

Webhook Channel Type

Using webhooks to send messages to external services.

  • Id: chtp_webhook
  • DisplayName: Webhook
  • Description: Send messages to external services using webhooks.
  • CreationMode: Manual
  • ContactTypeId: cntp_webhook
  • ConfigSchema: { "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:

FieldTypeDescription
IdstringUnique identifier for the channel.
TenantId?stringIdentifier of the tenant that owns the channel.
SharingModeChannelSharingModeSpecifies how the channel is shared.
DisplayNamestringHuman-readable name for the channel.
DescriptionstringDescription of the channel.
ChannelTypeIdstringIdentifier of the channel type.
ContactTypeIdstringIdentifier of the contact type.
ConfigstringEncrypted configuration data for the channel.
From?Contact JSONThe optional default sender for messages sent through this channel.
FromUserId?stringOptional identifier of the user associated with the default sender.
PriorityintPriority 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 when TenantId is 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:

FieldFilterSort 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:

FieldTypeDescription
CanUpdateboolIndicates if the channel can be modified by the requester.
CanDeleteboolIndicates 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.

FieldTypeDescription
IdstringUnique identifier for the message.
TenantId?stringIdentifier of the tenant that owns the message, if applicable.
SubjectstringSubject of the message.
TextContent?stringPlain text content of the message.
HtmlContent?stringHTML content of the message.
ResourcesResourceContent[]List of inline content or attachments to be included in the message.
DestinationsDestination[]List of destinations to which the message will be sent.
NotificationTypeModuleElementIdType of notification the message is associated with.
MessageThreadId?stringOptional identifier of the message thread the message belongs to.
CreatedAtDateTimeTimestamp 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:

FieldTypeDescription
IdstringUnique identifier for the content.
ContentTypestringMIME type of the inline content.
DispositionResourceContentDispositionIndicates if the content is inline or an attachment.
FileNamestringFile name of the inline content.
ContentFormatMessageContentFormatFormat of the Content field.
Contentbyte[]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, the Content field 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:

FieldTypeDescription
IdstringLocally unique identifier for the destination within the message.
ContactTypeIdstringIdentifier of the contact type for the destination.
From?Contact JSONThe optional sender of the message.
FromUserId?stringOptional identifier of the user associated with the sender contact.
SentDate?DateTimeTimestamp when the message was sent by the channel.
SentByChannelId?stringIdentifier of the channel that sent the message.
ContactContact JSONContact information for the recipient.
DisplayName?stringOptional human-readable name for the destination.
UserId?stringOptional identifier of the user associated with the contact.
GroupingDestinationGroupingSpecifies 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:

FieldTypeDescription
MessageIdstringIdentifier of the message.
UserIdstringIdentifier of the user.
ReadAt?DateTimeTimestamp when the message was read by the user.
ArchivedAt?DateTimeTimestamp 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:

FieldFilterSort 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 field UserId of 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 field FromUserId of 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:

FieldTypeDescription
IdstringUnique identifier for the message thread.
TenantId?stringIdentifier of the tenant that owns the message thread, if applicable.
EntityType?ModuleElementIdOptional type of the entity the thread is associated with.
EntityId?stringOptional identifier of the entity the thread is associated with.
CaptionstringHuman-readable caption for the message thread.
CreatedAtDateTimeTimestamp when the message thread was created.
ClosedAt?DateTimeTimestamp when the message thread was closed.
StartDate?DateTimeTimestamp when the first message was sent in the thread.
LastDate?DateTimeTimestamp when the last message was sent in the thread.
ParticipantsParticipant[]List of participants in the message thread.
MessageCountintTotal 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:

FieldTypeDescription
MessageThreadIdstringIdentifier of the message thread.
UserIdstringIdentifier of the user.
UnreadMessageCountintNumber 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:

FieldFilterSort 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:

FieldFilterSort by
MessageThreadId✅ required 1
UserId
CreatedAt
UpdatedAt
Content✅ text and html