Messages
Each civic issue has a threaded chat discussion. Messages are delivered in real-time via Socket.IO and are also accessible through the REST API for initial page loads and SSR scenarios.
GET /api/issues/:id/messagesβ
Retrieve the message thread for an issue, ordered by creation time ascending. No authentication required.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 50 | Number of messages per page (1β100) |
lastKey | string | β | Cursor from previous page's nextKey |
Request:
GET /api/issues/iss_01HN5K.../messages?limit=20
Response β 200 OK:
{
"success": true,
"data": {
"count": 7,
"items": [
{
"messageId": "msg_01HP...",
"issueId": "iss_01HN5K...",
"content": "I've been complaining about this for two months. BBMP keeps closing the complaint without fixing it.",
"author": {
"userId": "usr_01HN...",
"displayName": "Priya S.",
"avatarUrl": "https://lh3.googleusercontent.com/...",
"role": "citizen"
},
"reactions": {
"π": 12,
"π‘": 5,
"π": 3
},
"myReaction": null,
"createdAt": "2025-01-15T09:10:00.000Z",
"editedAt": null
},
{
"messageId": "msg_01HQ...",
"issueId": "iss_01HN5K...",
"content": "Escalated to the ward councillor. Reference number: BBMP/2025/0987.",
"author": {
"userId": "off_01HN...",
"displayName": "BBMP Ward Officer",
"avatarUrl": null,
"role": "official"
},
"reactions": {
"π": 28,
"β€οΈ": 7
},
"myReaction": "π",
"createdAt": "2025-01-15T11:30:00.000Z",
"editedAt": null
}
],
"nextKey": null,
"_links": {
"self": "/api/issues/iss_01HN5K.../messages?limit=20",
"next": null
}
}
}
The myReaction field is populated only when the request includes a valid Authorization header. It reflects which emoji the authenticated user has reacted with on that message, or null if they have not reacted.
POST /api/issues/:id/messagesβ
Post a new message to an issue thread. Authentication required.
Request:
POST /api/issues/iss_01HN5K.../messages
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"content": "The BBMP complaint portal shows this as 'resolved' but the light is still out. Attaching screenshot."
}
Validation rules:
content: required, 1β1000 characters, will be XSS-sanitized
Response β 201 Created:
{
"success": true,
"data": {
"messageId": "msg_01HR...",
"issueId": "iss_01HN5K...",
"content": "The BBMP complaint portal shows this as 'resolved' but the light is still out. Attaching screenshot.",
"author": {
"userId": "usr_01HS...",
"displayName": "Rahul M.",
"avatarUrl": "https://lh3.googleusercontent.com/...",
"role": "citizen"
},
"reactions": {},
"createdAt": "2025-01-16T08:15:00.000Z"
}
}
Immediately after the REST response, the server emits a message:new event to all clients in room:issue:iss_01HN5K... via Socket.IO. See Real-Time Architecture for the event payload.
DELETE /api/issues/:id/messages/:msgIdβ
Delete a message. Authentication required. The requesting user must be the message author or an admin.
Request:
DELETE /api/issues/iss_01HN5K.../messages/msg_01HR...
Authorization: Bearer <accessToken>
Response β 200 OK:
{
"success": true,
"data": {
"messageId": "msg_01HR...",
"deleted": true
}
}
Deleted messages are soft-deleted β the record is retained in DynamoDB with deleted: true and the content replaced with [message deleted]. This preserves conversation threading while removing the content.
Response β 403 (not the author or admin):
{
"success": false,
"error": {
"code": "FORBIDDEN",
"message": "You can only delete your own messages"
}
}
POST /api/issues/:id/messages/:msgId/reactβ
Add or toggle a reaction emoji on a message. Authentication required. Each user can have one active reaction per message. Sending the same emoji again removes the reaction (toggle). Sending a different emoji replaces the existing reaction.
Supported reactions: π β€οΈ π‘ π’ π π¨
Request:
POST /api/issues/iss_01HN5K.../messages/msg_01HR.../react
Authorization: Bearer <accessToken>
Content-Type: application/json
{
"emoji": "π"
}
Response β 200 OK:
{
"success": true,
"data": {
"messageId": "msg_01HR...",
"reactions": {
"π": 12,
"π‘": 5,
"π": 4
},
"myReaction": "π"
}
}
Response β 200 OK (toggled off β same emoji sent again):
{
"success": true,
"data": {
"messageId": "msg_01HR...",
"reactions": {
"π": 12,
"π‘": 5,
"π": 3
},
"myReaction": null
}
}
After the REST response, the server emits a message:reaction event to all clients in the issue room with the updated reaction counts. See Real-Time Architecture.
Response β 400 (unsupported emoji):
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "emoji must be one of: π β€οΈ π‘ π’ π π¨"
}
}
Real-Time Deliveryβ
Messages and reactions are delivered to connected clients in real-time via Socket.IO without polling. The REST endpoints are used for:
- Initial page load β fetch existing messages when a user first opens an issue
- Offline recovery β catch up on messages missed during a disconnection
Once the page has loaded and the Socket.IO connection is established, new messages arrive via the message:new event and new reactions via message:reaction. The client appends/updates these in local state without needing to re-fetch.
See Real-Time Architecture for event schemas and room management.