Messaging Specification
Overview
This document defines the asynchronous messaging module implemented for Poolia.
The feature introduces a thread-based communication model between pool owners and technicians while preserving the current REST-based API architecture and avoiding real-time chat complexity.
The module is designed to support operational communication within the context of a pool, including:
- message threads
- replies
- access control
- photo attachments stored in Azure Blob Storage
Objective
The objective of this feature is to provide a structured asynchronous communication channel where:
- both
ownerandtechniciancan open a conversation thread - both can reply within the same thread
- the conversation remains associated with a specific
pool - messages can include photo attachments
- the backend continues to operate with standard REST endpoints
- the frontend can consume normalized responses without additional orchestration complexity
This feature is not intended to behave as a real-time chat system.
Functional Model
The module is based on two main entities:
MessageThreadMessage
A thread represents a conversation context.
A message represents an individual entry inside that thread.
This design supports the required behavior without introducing bidirectional sockets or real-time message streaming.
Data Model
MessageThread
MessageThread represents the top-level conversation entity.
Main fields
poolId: identifier of the pool to which the thread belongscreatedBy: identifier of the user who opened the threadcreatedByRole: role of the user who opened the threadsubject: thread title or subjectstatus: current thread statusvisitId: optional visit referencelastMessageAt: timestamp of the latest messagelastMessagePreview: cached preview of the latest messagelastMessageAuthorId: author of the latest messagelastMessageAuthorRole: role of the latest message authorlastMessageType: type of the latest messagecreatedAtupdatedAt
Status values
Supported thread statuses:
openansweredclosed
Message
Message represents a message inside a thread.
Main fields
threadId: parent thread identifierpoolId: related pool identifierauthorId: author identifierauthorRole: role of the authormessageType:questionoranswerbody: text contentreplyToMessageId: optional reference to another message in the same threadcreatedAtupdatedAt
Photo
The existing Photo model is reused instead of introducing a new attachment entity.
Additional relations used by messaging
threadIdmessageId
This allows each uploaded photo to remain stored in Azure Blob Storage while keeping relational metadata in MongoDB.
Permissions and Access Control
Access to the messaging module is controlled through the user’s relationship to the pool.
A user can access a thread or message if at least one of the following conditions is true:
- the user is
admin - the user is
ownerand matchespool.owner_id - the user is
technicianand is included inpool.technician_ids
Authorization Rules
Thread creation
Allowed for:
ownertechnicianadmin
provided the user has access to the target pool.
Thread reading
Allowed for users with access to the related pool.
Message creation
Allowed for users with access to the related pool, as long as the thread is not closed.
Message editing
Allowed only for:
- the original message author
admin
Message photo upload
Allowed for users with access to the related pool, and only if the target message belongs to the target thread.
Thread State Logic
Thread state transitions are intentionally simple.
Rules
- a newly created thread starts as
open - if a new message of type
answeris added, the thread may move toanswered - if a thread is currently
answeredand a newquestionis added, it may return toopen - if a thread is
closed, no new messages may be added
This provides a clear operational distinction between unresolved, responded, and finalized conversations.
Last Message Cache
To optimize thread listing, MessageThread stores cached information about the most recent message:
lastMessageAtlastMessagePreviewlastMessageAuthorIdlastMessageAuthorRolelastMessageType
Purpose
This avoids reading all messages in a thread when rendering a conversation list.
Update behavior
The cache is refreshed every time a new message is created.
Frontend impact
The thread listing endpoint already contains enough data to render a useful inbox-style view without loading the entire conversation.
Message Photos
Design Decision
Message attachments are implemented using the existing Photo entity.
This avoids duplicating:
- storage logic
- Azure Blob integration
- metadata persistence
- signed URL generation patterns
Storage Model
Photos remain external files stored in Azure Blob Storage.
MongoDB stores only metadata and relations to:
poolIdthreadIdmessageId
Blob path convention
Message photo uploads follow a path format similar to:
messages/{threadId}/{messageId}/{timestamp}_{filename}
This preserves a predictable and traceable storage structure.
API Endpoints
Create Thread
POST /pools/{pool_id}/threads
Request body
{
"subject": "Consulta sobre el cloro",
"messageType": "question",
"body": "Vi el valor bajo en la última visita, hay que corregir hoy?"
}
Behavior
- validates pool access
- creates a
MessageThread - creates the first
Message - updates the thread cache
List Threads by Pool
GET /pools/{pool_id}/threads
Optional filter:
GET /pools/{pool_id}/threads?status=open
Response content
The thread list includes:
- thread metadata
- status
- subject
- creator info
- cached latest message info
- message count
- timestamps
Get Thread
GET /threads/{thread_id}
Returns the thread metadata.
Update Thread
PATCH /threads/{thread_id}
Example
{
"status": "closed"
}
Typical usage includes closing a thread or updating its subject.
List Messages in Thread
GET /threads/{thread_id}/messages
Behavior
Returns the ordered list of messages for the thread.
Each message includes an embedded photos array.
Optimization
This endpoint avoids the N+1 query pattern by:
- loading all messages in the thread
- loading all related message photos for the thread in a single query
- grouping photos in memory by
messageId
This makes the endpoint suitable for normal thread rendering in the frontend.
Get Single Message
GET /messages/{message_id}
Returns a single message with embedded photos.
Create Message
POST /threads/{thread_id}/messages
Request body
{
"messageType": "answer",
"body": "Sí, hoy lo ajusto."
}
Behavior
- validates access
- checks that the thread is not closed
- creates the message
- updates thread status if required
- refreshes the thread cache
- returns the created message with
photos: []
Update Message
PATCH /messages/{message_id}
Request body
{
"body": "Sí, hoy lo ajusto y mañana vuelvo a medir."
}
Behavior
- validates access
- ensures the current user is either the author or an admin
- updates the message body
- refreshes the thread cache if the updated message is the latest one
- returns the updated message with embedded photos
Upload Photo to Message
POST /threads/{thread_id}/messages/{message_id}/photos
This endpoint is the preferred public entrypoint for message attachments.
Content type
multipart/form-data
Fields
filedescription(optional)
Behavior
- validates pool access
- validates that the message belongs to the thread
- uploads the file to Azure Blob Storage
- creates the
Photometadata entry - returns the updated
MessageResponse, including the fullphotosarray
Reasoning
This design lets the frontend update a single message in local state without refreshing the entire thread.
Response Model
The frontend receives messages in a normalized structure that includes embedded photos.
Example response
{
"id": "6610d3f16c5cfc2c8fd00001",
"threadId": "6610d3d66c5cfc2c8fd00000",
"poolId": "660ffae16c5cfc2c8fd12345",
"authorId": "660ffae16c5cfc2c8fd99999",
"authorRole": "owner",
"messageType": "question",
"body": "Te adjunto fotos del estado actual.",
"replyToMessageId": null,
"createdAt": "2026-04-03T18:10:00Z",
"updatedAt": null,
"photos": [
{
"id": "6610d4116c5cfc2c8fd00002",
"fileName": "agua.jpg",
"blobPath": "messages/6610d3d66c5cfc2c8fd00000/6610d3f16c5cfc2c8fd00001/1712167800_agua.jpg",
"contentType": "image/jpeg",
"size": 245881,
"userId": null,
"visitId": null,
"poolId": "660ffae16c5cfc2c8fd12345",
"threadId": "6610d3d66c5cfc2c8fd00000",
"messageId": "6610d3f16c5cfc2c8fd00001",
"uploadedBy": "660ffae16c5cfc2c8fd99999",
"createdAt": "2026-04-03T18:11:00Z",
"description": "Estado hoy",
"url": "https://..."
}
]
}
Backend Flow Summary
Thread creation flow
- validate access to the pool
- create
MessageThread - create first
Message - update thread cache
Message creation flow
- validate access to the pool
- verify thread status
- create
Message - update thread status if needed
- refresh thread cache
- return
MessageResponse
Photo upload flow
- validate access to the pool
- verify thread-message relationship
- upload file to Azure Blob Storage
- persist
Photometadata in MongoDB - return updated
MessageResponse
Frontend Integration
Thread list screen
Endpoint
GET /pools/{pool_id}/threads
Purpose
Used to render the list of conversation threads associated with a pool.
Suggested UI fields
- subject
- status
- last message preview
- last message author role
- last message date
- message count
Thread detail screen
Endpoint
GET /threads/{thread_id}/messages
Purpose
Used to render the full conversation for a thread.
Each returned message already includes its photo attachments.
Suggested UI per message
- author
- role
- message type
- body
- created date
- photo gallery or thumbnails
Creating a message with attachments
Recommended frontend flow:
- create the message using
POST /threads/{thread_id}/messages - obtain the returned
messageId - upload one or more photos using
POST /threads/{thread_id}/messages/{message_id}/photos - replace the local message state with the updated message returned by the backend
This keeps message creation simple and avoids sending complex multipart requests containing both structured message payload and files.
Suggested Frontend State Strategy
A simple and effective frontend approach is:
- load the thread with
GET /threads/{thread_id}/messages - keep messages in local state
- append newly created messages directly from the creation response
- replace a message by
idafter a photo upload using the updated message returned by the attachment endpoint
This keeps the UI responsive and minimizes unnecessary thread reloads.
Non-Goals
The current implementation does not attempt to provide:
- real-time chat
- WebSocket transport
- typing indicators
- online presence
- read receipts
- unread counters per user
- push notifications
These capabilities may be added later if needed, but they are outside the scope of the current design.
Summary
The Poolia messaging module is implemented as an asynchronous, thread-based REST feature scoped to a pool.
It allows both owners and technicians to initiate and continue conversations, supports message replies and photo attachments, reuses the existing Azure Blob Storage photo flow, and returns normalized data structures that simplify frontend integration.
The design intentionally favors clarity, consistency, and maintainability over real-time behavior.