Resource: Chat (messaging)
Tag: Chat · Module: chat
Source of truth for the chat surface is
centrifugo_chat_poc.md(the Centrifugo Chat POC). Chat is mid-migration off Supabase Realtime / PubNub onto self-hosted Centrifugo with a transactional outbox. This file maps the target client-facing endpoints onto the conventions (C0–C9) — it does not re-spec that design.
Chat messaging only. The shared Centrifugo connection token lives in realtime.md;
user-status presence is its own resource (presence.md). The current chat
(CreateDM/getToken for Supabase) + PubNub client are being replaced — do not spec the soon-dead
Supabase/PubNub surface; the contract targets the Centrifugo endpoints.
Operations — target endpoint map
Client-facing ops, aligned to conventions. Detailed request/response shapes are defined as each is built per the design doc — this is the operation inventory + the convention rules they must follow, not a re-spec.
| OperationID | Method + Path | Design § | Notes |
|---|---|---|---|
get-subscribe-token | POST /chat/subscribe-token | §5 | { conversationId } → per-chat:-channel JWT (the connection token lives in Realtime). TODO: Check if we really need this endpoint. |
send-message | POST /chat/messages/send | §7 | { conversationId, content, clientMsgId? }; backend fans out via outbox |
list-messages | POST /chat/messages/list | §7 | history; bidirectional cursor (before/after/around a message id — scrollback, jump-to-unread/pin/hit); {data, page} |
search-messages | POST /chat/messages/search | §6 | in-channel search overlay (optional global); { conversationId?, query, … } → {data, page} |
edit-message | POST /chat/messages/{id}/update | §10 | soft edit |
delete-message | POST /chat/messages/{id}/delete | §10 | soft delete |
send-typing | POST /chat/typing/send | §10 | ephemeral (direct publish, not outbox) |
add-reaction | POST /chat/messages/{id}/reactions/add | §10 | |
remove-reaction | POST /chat/messages/{id}/reactions/remove | §10 | |
pin-message | POST /chat/messages/{id}/pin | §4 | pin to the conversation |
unpin-message | POST /chat/messages/{id}/unpin | §4 | |
list-conversations | POST /chat/conversations/list | §7 | recent DMs / sidebar; {data, page} |
create-conversation | POST /chat/conversations/create | §6 | DM / group / private / org-wide (one body, visibility/historyMode) |
get-conversation | POST /chat/conversations/{id}/get | §6 | |
update-conversation | POST /chat/conversations/{id}/update | §6 | rename/describe |
archive-conversation | POST /chat/conversations/{id}/archive | §6 | |
convert-conversation | POST /chat/conversations/{id}/convert | §6 | group → private-channel (2-col UPDATE) |
add-conversation-member | POST /chat/conversations/{id}/members/add | §6 | admin only |
remove-conversation-member | POST /chat/conversations/{id}/members/remove | §6 | admin only |
leave-conversation | POST /chat/conversations/{id}/leave | §6 | |
mark-conversation-read | POST /chat/conversations/{id}/read | §7 | read-cursor update (per-member) |
update-conversation-preferences | POST /chat/conversations/{id}/preferences/update | §5 | per-member state { starred?, muteUntil?, notifyLevel? } — NOT update-conversation (which sets shared name/desc/visibility) |
list-pinned-messages | POST /chat/conversations/{id}/pins/list | §4/§7 | pinned bar + Details → Pinned; {data, page} |
list-conversation-attachments | POST /chat/conversations/{id}/attachments/list | §7 | Details → Shared files grid; {data, page} |
chat-attachment-upload-ticket | POST /chat/attachments/upload-ticket | §10 | shared image/file upload pattern (R2) |
chat-attachment-finalize | POST /chat/attachments/finalize | §10 | link to message |
create-conversation absorbs the legacy CreateDM (POST /api/v1/chat/conversations).
Notes on the additions / model decisions (audited against the Sidebar Chat handoff):
- Per-member vs shared conversation state.
update-conversation-preferencescarries the user's own star / mute-timer / notify-level (§5–§7) and is deliberately separate fromupdate-conversation(shared name/desc/visibility, admin-only).muteUntil= RFC3339 (or null = unmuted / sentinel for "until I turn it back on");notifyLevel=all | mentions | everyone | none(DMs:all | none).mark-readstays its own op (high-frequency cursor). - Global/public channels are auto-membership — every org member is implicitly in
globalchannels, so there is nojoin/ browse-channels op;leave-conversationon such a channel hides/mutes it for the user.privatechannels gain members only viaadd-conversation-member. - Link previews are embedded on the message, not a separate endpoint. The server unfurls at send time
(async, via the outbox) and attaches
linkPreviews[]to the message DTO, delivered on the realtime update (Slack/Discord model). Shape owned by the design doc; flagged here because the unfurl is a backend job, not client-side (browsers can't fetch arbitrary cross-origin OG tags). - Deferred (DM details, §7):
block-user/unblock-userandclear-conversationare out of the contract for now — added when the DM block/clear affordances ship (would extend C1b withblock/clear).
NOT in the typed client contract
- The outbox table, Centrifugo config, control calls (
/api/unsubscribe|disconnect, §5/§8) — server-side infra, not client ops. - Centrifugo's own history buffer — never a client API (§4).
- The
status:-channel subscribe-proxy is presence infra — see presence.md.
Conventions applied
- IDs: conversation/message ids are int64 today → stringified (C2); user ids are the canonical
stringified user id (matches
Member/User/occupants). - Timestamps: RFC3339 (C3).
- Lists (
list-messages,list-conversations,search-messages,list-pinned-messages,list-conversation-attachments):{ data, page }(C4);list-messagespages bidirectionally (before/after/arounda message id — scrollback + the §7 delta-fetch/jump model). - Paths: explicit-verb, sub-resourced (
/chat/conversations/{id}/members/add) per C1; CRUD bare-verb. - Attachments reuse the shared image/file upload-ticket→finalize pattern (user.md §Image upload).
Module / backend notes
chatcarries over (DM creation, repo/service) and grows the lifecycle/history services relocating from Postgres triggers/RPCs into Go (design §2, §7, §15). The connection token →realtime(realtime.md); presence →presence(presence.md).- Build order, schema, outbox, rollout sequencing: all governed by the design doc — not duplicated here.
- Detailed DTOs per op are finalized as the Centrifugo phases land; they must conform to the conventions above.