Messaging Domain — Story Bundles¶
MSG-STORY-001 — Send a direct message ✅ [Supporting]¶
AS A logged-in user I WANT to send a direct message to another user SO THAT I can have private conversations on the platform
SCENARIO 1: Successful message sent in existing conversation¶
Scenario ID: MSG-STORY-001-S1
GIVEN
The authenticated user has an existing conversation with the recipient
The message text is non-empty
WHEN
The user submits a message in that conversation
THEN
The message is persisted in the
messagingschemaThe API returns
201 Createdwith the new message object
SCENARIO 2: Message sent creates new conversation¶
Scenario ID: MSG-STORY-001-S2
GIVEN
No conversation exists between the sender and recipient
The recipient exists in the Users Service
WHEN
The user sends a message to the recipient for the first time
THEN
A new conversation record is created
The message is persisted within that conversation
The API returns
201 Createdwith the message andconversation_id
SCENARIO 3: Message to non-existent recipient rejected¶
Scenario ID: MSG-STORY-001-S3
GIVEN
The recipient user ID does not exist in the Users Service
WHEN
The user attempts to send a message to that ID
THEN
The API returns
404 Not Foundwith error codeUSER_NOT_FOUNDNo message or conversation is created
SCENARIO 4: Empty message rejected¶
Scenario ID: MSG-STORY-001-S4
GIVEN
The user submits a message with empty or whitespace-only text
WHEN
The request reaches the Messaging Service
THEN
The API returns
422 Unprocessable Entitywith error codeMESSAGE_TEXT_REQUIRED
SCENARIO 5: Unauthenticated request rejected¶
Scenario ID: MSG-STORY-001-S5
GIVEN
The request carries no JWT or an expired JWT
WHEN
POST /conversations/{id}/messagesis called
THEN
The API returns
401 Unauthorized
Architecture reference: Chapter 1 — Introduction and Goals, Chapter 5 — Building Block View, Chapter 6 — Runtime View
MSG-FE-001.1 — Conversation and message compose UI¶
AS A logged-in user I WANT a conversation view with a message input at the bottom SO THAT I can read and send messages in a single screen
SCENARIO 1: Sending a message appends it to the conversation¶
Scenario ID: MSG-FE-001.1-S1
GIVEN
The user is viewing a conversation at
/conversations/{id}The message input contains non-empty text
WHEN
The user clicks “Send” or presses Enter
THEN
A
POST /conversations/{id}/messagesrequest is sent with the Bearer JWTOn
201 Createdthe new message is appended to the conversation viewThe input field is cleared
SCENARIO 2: Starting a new conversation from a user profile¶
Scenario ID: MSG-FE-001.1-S2
GIVEN
The user is viewing another user’s profile
WHEN
The user clicks “Message”
THEN
A
POST /conversationsrequest is sent with{recipient_id}The user is navigated to the new or existing conversation view
Architecture reference: Chapter 3 — Context and Scope
MSG-FE-001.2 — Message input validation¶
AS A logged-in user I WANT the message input to prevent empty submissions SO THAT I cannot accidentally send blank messages
MSG-BE-001.1 — POST /conversations endpoint¶
AS A Messaging Service
I WANT to expose POST /conversations
SO THAT a conversation can be created or retrieved between two users
SCENARIO 1: New conversation created¶
Scenario ID: MSG-BE-001.1-S1
GIVEN
The request carries a valid JWT
The body contains
{recipient_id}for a user that exists in the Users ServiceNo conversation between sender and recipient exists
WHEN
POST /conversationsis called
THEN
The Messaging Service calls
GET /users/{recipient_id}to verify the recipient existsA row is inserted into
messaging.conversationsA row is inserted into
messaging.conversation_participantsfor both usersResponse is
201 Createdwith{conversation_id, participants}
SCENARIO 2: Existing conversation returned (idempotent)¶
Scenario ID: MSG-BE-001.1-S2
GIVEN
A conversation between sender and recipient already exists
WHEN
POST /conversationsis called again with the samerecipient_id
THEN
No new conversation is created
Response is
200 OKwith the existing{conversation_id, participants}
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View
MSG-BE-001.2 — POST /conversations/{id}/messages endpoint¶
AS A Messaging Service
I WANT to expose POST /conversations/{id}/messages
SO THAT authenticated users can send messages within a conversation
SCENARIO 1: Message persisted and returned¶
Scenario ID: MSG-BE-001.2-S1
GIVEN
The request carries a valid JWT
The caller is a participant in conversation
{id}The body contains
{text}that is non-empty
WHEN
POST /conversations/{id}/messagesis called
THEN
A row is inserted into
messaging.messages (id, conversation_id, sender_id, text, created_at)Response is
201 Createdwith{id, conversation_id, sender_id, text, created_at}
SCENARIO 2: Non-participant cannot send message¶
Scenario ID: MSG-BE-001.2-S2
GIVEN
The authenticated user is not a participant in conversation
{id}
WHEN
POST /conversations/{id}/messagesis called
THEN
Response is
403 Forbiddenwith error codeNOT_A_PARTICIPANT
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View
MSG-INFRA-001.1 — Messaging Service containerised and deployable¶
AS AN operator I WANT the Messaging Service packaged as a Docker image SO THAT it can be deployed independently behind the nginx reverse proxy
SCENARIO 1: Container starts and serves traffic¶
Scenario ID: MSG-INFRA-001.1-S1
GIVEN
A Docker image is built from the Messaging Service source
DATABASE_URLandJWT_PUBLIC_KEYare set as environment variables
WHEN
The container starts
THEN
The service is reachable on its configured port
GET /healthreturns200 OK
Architecture reference: Chapter 7 — Deployment View
MSG-INFRA-001.2 — Messaging schema migration¶
AS AN operator
I WANT an Alembic migration that creates the messaging schema tables
SO THAT the Messaging Service can persist conversations and messages
SCENARIO 1: Migration creates required tables¶
Scenario ID: MSG-INFRA-001.2-S1
GIVEN
A PostgreSQL instance has no
messagingschema tables
WHEN
alembic upgrade headis run for the Messaging Service
THEN
Tables
messaging.conversations,messaging.conversation_participants, andmessaging.messagesexistmessaging.messageshas an index on(conversation_id, created_at DESC)Migration is idempotent
Architecture reference: Chapter 7 — Deployment View, Chapter 9 — Architecture Decisions
MSG-INFRA-001.3 — Event handling¶
Not applicable at this stage — message delivery is synchronous. If push notifications or unread-count events are added later, this sub-story should be revisited.
MSG-INFRA-001.4 — Monitoring and alarms for Messaging Service¶
AS AN operator I WANT a health check endpoint and an error-rate alert on the Messaging Service SO THAT I am notified when messaging is degraded
SCENARIO 1: Health check returns 200¶
Scenario ID: MSG-INFRA-001.4-S1
GIVEN
The Messaging Service is running and connected to its database
WHEN
GET /healthis called
THEN
Response is
200 OKwith{"status": "ok"}
SCENARIO 2: Alert fires on elevated error rate¶
Scenario ID: MSG-INFRA-001.4-S2
GIVEN
Prometheus scrapes
/metricson the Messaging Service every 15 seconds
WHEN
The HTTP 5xx error rate exceeds 1% for 5 consecutive minutes
THEN
An alert fires and is routed to the on-call channel
Architecture reference: Chapter 7 — Deployment View, Chapter 8 — Cross-Cutting Concepts
MSG-STORY-002 — Read a conversation ✅ [Supporting]¶
AS A logged-in user I WANT to open a conversation and read its message history SO THAT I can follow the thread of a private exchange
SCENARIO 1: Conversation messages returned in order¶
Scenario ID: MSG-STORY-002-S1
GIVEN
The authenticated user is a participant in the conversation
The conversation has at least one message
WHEN
The user opens the conversation
THEN
Messages are returned sorted by
created_at ASC, paginated (default 50 per page)Each message includes
sender_id,text, andcreated_at
SCENARIO 2: Non-participant cannot read conversation¶
Scenario ID: MSG-STORY-002-S2
GIVEN
The authenticated user is not a participant in the conversation
WHEN
GET /conversations/{id}/messagesis called
THEN
The API returns
403 Forbiddenwith error codeNOT_A_PARTICIPANT
SCENARIO 3: Conversation list shows all user conversations¶
Scenario ID: MSG-STORY-002-S3
GIVEN
The authenticated user has one or more conversations
WHEN
GET /conversationsis called
THEN
A list of conversations is returned, each with the other participant’s profile and the latest message preview
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View
MSG-FE-002.1 — Conversations list and message thread view¶
AS A logged-in user I WANT a conversations inbox and a message thread view SO THAT I can navigate between conversations and read messages
SCENARIO 1: Inbox lists conversations with latest message preview¶
Scenario ID: MSG-FE-002.1-S1
GIVEN
The user navigates to
/conversations
WHEN
The page mounts
THEN
GET /conversationsis called and each conversation is shown with the other participant’s avatar, username, and latest message snippet
SCENARIO 2: Opening a conversation loads message history¶
Scenario ID: MSG-FE-002.1-S2
GIVEN
The user clicks a conversation in the inbox
WHEN
The conversation view opens
THEN
GET /conversations/{id}/messagesis called and messages are rendered oldest-first, scrolled to the bottom
Architecture reference: Chapter 3 — Context and Scope
MSG-BE-002.1 — GET /conversations and GET /conversations/{id}/messages endpoints¶
AS A Messaging Service
I WANT to expose GET /conversations and GET /conversations/{id}/messages
SO THAT participants can list their conversations and read message history
SCENARIO 1: Conversations list returned¶
Scenario ID: MSG-BE-002.1-S1
GIVEN
The request carries a valid JWT
WHEN
GET /conversationsis called
THEN
Response is
200 OKwith conversations where the caller is a participant, each enriched with the other participant’s public profile (via Users Service) and the latest message
SCENARIO 2: Message history returned paginated¶
Scenario ID: MSG-BE-002.1-S2
GIVEN
The caller is a participant in conversation
{id}
WHEN
GET /conversations/{id}/messages?page=1&page_size=50is called
THEN
Response is
200 OKwith{items: [...], page, page_size, has_next}sortedcreated_at ASC
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View
MSG-INFRA-002.1 — Messaging Service containerised and deployable¶
Covered by MSG-INFRA-001.1.
Architecture reference: Chapter 7 — Deployment View
MSG-INFRA-002.2 — Data store¶
Covered by MSG-INFRA-001.2 — the messaging.messages table and its index on (conversation_id, created_at) already support read queries.
Architecture reference: Chapter 7 — Deployment View
MSG-INFRA-002.3 — Event handling¶
Not applicable — reading messages is a synchronous read.
MSG-INFRA-002.4 — Monitoring¶
Covered by MSG-INFRA-001.4.
Architecture reference: Chapter 7 — Deployment View
MSG-STORY-003 — Mention a user in a message ✅ [Supporting]¶
AS A logged-in user I WANT to @mention another user in a message SO THAT they are notified and can see the context of the mention
SCENARIO 1: Mention detected and stored¶
Scenario ID: MSG-STORY-003-S1
GIVEN
The message text contains
@usernamefor an existing user
WHEN
The message is sent
THEN
The Messaging Service resolves the username to a
user_idvia the Users ServiceA row is inserted into
messaging.mentions (message_id, target_user_id)
SCENARIO 2: Unknown @handle silently ignored¶
Scenario ID: MSG-STORY-003-S2
GIVEN
The message text contains
@nonexistentuser
WHEN
The message is sent
THEN
The message is saved normally
No mention record is created for the unknown handle
No error is returned to the sender
SCENARIO 3: Multiple mentions in one message¶
Scenario ID: MSG-STORY-003-S3
GIVEN
The message text contains two or more valid
@usernamehandles
WHEN
The message is sent
THEN
A mention record is created for each resolved user
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View
MSG-FE-003.1 — @mention autocomplete in message input¶
AS A logged-in user
I WANT an autocomplete dropdown when I type @ in the message input
SO THAT I can mention users without knowing their exact handle
SCENARIO 1: Autocomplete appears on @ trigger¶
Scenario ID: MSG-FE-003.1-S1
GIVEN
The user is typing in the message input
WHEN
The user types
@followed by at least one character
THEN
A dropdown appears with matching usernames returned by the Users domain username search / handle resolution capability
Selecting a suggestion inserts the full
@usernameinto the input
Cross-domain dependency: This scenario depends on the Users-domain handle resolution endpoint GET /users?username={handle} as documented in the runtime view; any future Users story inventory entry should align with that contract rather than redefining it here.
Architecture reference: Chapter 3 — Context and Scope; Chapter 6 — Runtime View
MSG-BE-003.1 — Mention detection and storage on message send¶
AS A Messaging Service
I WANT to parse @username handles from message text after a message is saved
SO THAT mentions are recorded for notification purposes
SCENARIO 1: Mention resolved and stored¶
Scenario ID: MSG-BE-003.1-S1
GIVEN
The message text contains
@validuserThe Users domain can resolve
validuserto a matching user
WHEN
The message is persisted
THEN
The Messaging Service calls the Users-domain handle-resolution contract for
validuserA row is inserted into
messaging.mentions (message_id, target_user_id)
SCENARIO 2: Mention resolution failure does not fail message send¶
Scenario ID: MSG-BE-003.1-S2
GIVEN
The Users Service is unreachable during mention resolution
WHEN
The message is sent with an
@handle
THEN
The message is saved successfully
Mention resolution failure is logged with
trace_idThe sender receives
201 Created(mention is best-effort)
Architecture reference: Chapter 5 — Building Block View, Chapter 6 — Runtime View, Chapter 8 — Cross-Cutting Concepts
MSG-INFRA-003.1 — Messaging Service containerised and deployable¶
Covered by MSG-INFRA-001.1.
Architecture reference: Chapter 7 — Deployment View
MSG-INFRA-003.2 — mentions table migration¶
AS AN operator
I WANT an Alembic migration that creates the messaging.mentions table
SO THAT mention records can be persisted
SCENARIO 1: Migration creates mentions table¶
Scenario ID: MSG-INFRA-003.2-S1
GIVEN
The
messagingschema exists (MSG-INFRA-001.2 has run)
WHEN
The migration for MSG-STORY-003 is applied
THEN
Table
messaging.mentionsexists with columns(id, message_id, target_user_id, created_at)A foreign key from
message_idtomessaging.messages(id)withON DELETE CASCADEexists
Architecture reference: Chapter 7 — Deployment View
MSG-INFRA-003.3 — Event handling¶
AS AN operator I WANT mention resolution to be fault-tolerant with respect to the Users Service SO THAT a Users Service outage does not degrade message sending
SCENARIO 1: Mention resolution logged on failure without blocking response¶
Scenario ID: MSG-INFRA-003.3-S1
GIVEN
The Users Service returns a non-2xx response or times out during mention resolution
WHEN
The Messaging Service calls the Users handle-resolution contract for
{handle}
THEN
The error is caught, logged with
trace_id, and the message send completes with201 CreatedNo mention row is inserted for the unresolved handle
Architecture reference: Chapter 7 — Deployment View, Chapter 8 — Cross-Cutting Concepts
MSG-INFRA-003.4 — Monitoring¶
Covered by MSG-INFRA-001.4.
Architecture reference: Chapter 7 — Deployment View