Skip to content

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 owner and technician can 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:

  • MessageThread
  • Message

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 belongs
  • createdBy: identifier of the user who opened the thread
  • createdByRole: role of the user who opened the thread
  • subject: thread title or subject
  • status: current thread status
  • visitId: optional visit reference
  • lastMessageAt: timestamp of the latest message
  • lastMessagePreview: cached preview of the latest message
  • lastMessageAuthorId: author of the latest message
  • lastMessageAuthorRole: role of the latest message author
  • lastMessageType: type of the latest message
  • createdAt
  • updatedAt

Status values

Supported thread statuses:

  • open
  • answered
  • closed

Message

Message represents a message inside a thread.

Main fields

  • threadId: parent thread identifier
  • poolId: related pool identifier
  • authorId: author identifier
  • authorRole: role of the author
  • messageType: question or answer
  • body: text content
  • replyToMessageId: optional reference to another message in the same thread
  • createdAt
  • updatedAt

Photo

The existing Photo model is reused instead of introducing a new attachment entity.

Additional relations used by messaging

  • threadId
  • messageId

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 owner and matches pool.owner_id
  • the user is technician and is included in pool.technician_ids

Authorization Rules

Thread creation

Allowed for:

  • owner
  • technician
  • admin

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 answer is added, the thread may move to answered
  • if a thread is currently answered and a new question is added, it may return to open
  • 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:

  • lastMessageAt
  • lastMessagePreview
  • lastMessageAuthorId
  • lastMessageAuthorRole
  • lastMessageType

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:

  • poolId
  • threadId
  • messageId

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:

  1. loading all messages in the thread
  2. loading all related message photos for the thread in a single query
  3. 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

  • file
  • description (optional)

Behavior

  • validates pool access
  • validates that the message belongs to the thread
  • uploads the file to Azure Blob Storage
  • creates the Photo metadata entry
  • returns the updated MessageResponse, including the full photos array

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

  1. validate access to the pool
  2. create MessageThread
  3. create first Message
  4. update thread cache

Message creation flow

  1. validate access to the pool
  2. verify thread status
  3. create Message
  4. update thread status if needed
  5. refresh thread cache
  6. return MessageResponse

Photo upload flow

  1. validate access to the pool
  2. verify thread-message relationship
  3. upload file to Azure Blob Storage
  4. persist Photo metadata in MongoDB
  5. 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:

  1. create the message using POST /threads/{thread_id}/messages
  2. obtain the returned messageId
  3. upload one or more photos using POST /threads/{thread_id}/messages/{message_id}/photos
  4. 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 id after 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.