Chat
1:1 & group messaging with realtime events
The chat block turns on a complete in-app messaging system: direct (1:1) and group conversations, message editing and deletion, typing indicators, read receipts with unread counts, and file attachments — all served over REST routes under basePath, with realtime events broadcast through the pubsub block.
Like everything in Nucleus, it pairs with a ready frontend: the ChatPanel component and useChat hook consume these routes and events directly, so a working messenger is a config key plus one component.
Configuration#
Defaults are production-sensible — enabling the block is usually enough. The knobs below tune group size, message limits and which social signals are broadcast. (The JSON below shows illustrative custom values, not defaults — e.g. the real defaults are maxGroupParticipants 256 and attachments.maxPerMessage 10.)
1{2 "chat": {3 "enabled": true,4 "basePath": "/chat",5 "groupsEnabled": true,6 "maxGroupParticipants": 100,7 "maxMessageLength": 8000,8 "typingIndicators": true,9 "readReceipts": true,10 "attachments": { "enabled": true, "maxPerMessage": 5 }11 }12}enabledbooleanOptionalMaster switch. Routes register only when enabled and a database is configured.
falsebasePathstringOptionalMount point for the chat REST routes.
"/chat"groupsEnabledbooleanOptionalAllow group conversations in addition to 1:1 direct chats. Groups require a title; the creator is assigned the 'owner' role and everyone else 'member'. An 'admin' role is also defined and may manage participants (owner OR admin), but no endpoint currently assigns it — it's dormant.
truemaxGroupParticipantsnumberOptionalCap on participants per group conversation.
256maxMessageLengthnumberOptionalMaximum message length in characters, enforced on send and edit.
8000editingEnabled / deletionEnabledbooleanOptionalLet senders edit or delete their own messages. Deletion is soft — content is cleared and deletedAt set, so the conversation history stays coherent.
truetypingIndicators / readReceiptsbooleanOptionalBroadcast typing and read events to other participants over the realtime channel. readReceipts gates only the conversation.read broadcast — each participant's unreadCount is tracked independently of this flag (incremented on incoming messages, reset to 0 when they mark the conversation read), so setting readReceipts=false does NOT disable unreadCount tracking.
truepresencebooleanOptionalReserved flag — currently inert. It is accepted and merged into the chat config but gates no chat-specific broadcast: the chat broadcaster only emits message/conversation/typing/participant events. Live online/offline presence is delivered by the pubsub layer at the connection level (config.pubsub.presence.enabled), so setting chat.presence=false does NOT disable presence.
truemoderatorRolesstring[]OptionalRoles allowed to delete ANY message alongside godmin. Deletion is otherwise sender-only; empty (default) keeps hard moderation godmin-only. When a caller holds one of these roles, a DELETE skips both the sender-only check and the deletionEnabled flag, and the action is logged at warn level. Membership is read from the token's x-user-roles on each request, so losing the role stops moderation on the next call — not at next login. Only affects delete; editing stays strictly sender-only.
[]realtimeTopicPrefixstringOptionalTopic prefix for the realtime events listed below.
"chat"messagePageSizenumberOptionalMessages per history page. The messages endpoint cursor-paginates newest-first with limit clamped to 1–100.
50attachments{ enabled?, maxPerMessage?, maxFileSizeBytes?, allowedMimeTypes? }OptionalFile attachments on messages. Requires storage.enabled and storage.cdn.enabled; since enabled defaults to true, attachments switch on automatically once storage + CDN are configured. Only enabled and maxPerMessage are enforced — maxFileSizeBytes and allowedMimeTypes are reserved/currently inert: they are accepted and merged into the chat config but read by no consumer, so setting them has no effect. Attachment size and MIME validation are governed solely by the storage block (storage.maxFileSizeBytes / storage.allowedMimeTypes).
Endpoints#
All under basePath; every route reads the authenticated user from the request (401 without one). Conversation responses include participants, your participant record and your unreadCount.
GET /conversationslistYour conversations, ordered by last activity. limit (default 30, max 100) + offset.
POST /conversationscreateDirect ({ targetUserId }) or group ({ type: 'group', title (required), participantIds, plus optional description and avatarFileId (must be a file the creator owns, else 403) }). Direct pairs are deduplicated — re-creating an existing pair returns the existing conversation.
GET /conversations/:idfetch oneFetch a single conversation you belong to, including participants, your participant record and unreadCount.
GET /conversations/:id/messageshistoryNewest-first cursor pagination: limit + before=<messageId> returns { messages, hasMore }.
POST /conversations/:id/messagessendSend text and/or pre-uploaded attachment ids (each must be a file the sender owns, else 403), optionally replying to another message (replyToId) and tagging a clientMessageId that is echoed back on the created message for client-side matching. The body also accepts an optional contentType (text/image/file/video/audio; auto-inferred from the first attachment's kind when omitted) and a metadata object (capped at 4096 code units of its JSON-serialized string length — equivalent to 4096 bytes only for ASCII content). A multipart sibling — POST .../messages/upload — uploads files and sends in one call. Send is ATOMIC: the message insert, attachment validation/persistence, and the conversation lastMessage + per-participant unreadCount updates run in a single DB transaction, so a failed attachment (403 unowned / 404 missing / 409 disabled / 400 too-many) rolls the whole send back — no orphaned message is persisted. The realtime broadcast fires only after the transaction commits.
POST /conversations/:id/read · /typingsignalsMark the conversation read (resets your unreadCount) and broadcast typing state. The read endpoint takes an optional { messageId } to mark read up to a specific message, defaulting to the latest message when omitted; the resulting lastReadMessageId is stored on your participant record and broadcast in the conversation.read event (when readReceipts is enabled) to drive other participants' read receipts.
PATCH · DELETE /messages/:idedit / deleteEdit or soft-delete your own messages — sender-only, enforced server-side. Exception: on DELETE, godmin and any role in chat.moderatorRoles may delete ANY message (deletionEnabled does not apply to them), and the action is audit-logged at warn level. PATCH/edit stays strictly sender-only.
POST /conversations/:id/participants · DELETE /conversations/:id/participants/:userIdmembershipAdd participants (POST) or remove one (DELETE .../:userId). Only owner/admin can manage others; any member can leave.
From the frontend#
The frontend kit ships a complete messenger UI on top of these routes — conversation list, message thread, composer with attachments, typing and presence — themeable like every Nucleus component.
ChatPanelcomponentThe full messenger surface. Pairs with useChat / useChatStore and the NewConversationModal; styling via chatPanelTheme / extendChatPanelTheme.
usePubSubhookThe realtime hook the panel uses under the hood — subscribe to the chat topics yourself if you build a custom UI.
Related sections