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.)

config.nucleus.json — chat
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}
enabledbooleanOptional

Master switch. Routes register only when enabled and a database is configured.

Defaultfalse
basePathstringOptional

Mount point for the chat REST routes.

Default"/chat"
groupsEnabledbooleanOptional

Allow 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.

Defaulttrue
maxGroupParticipantsnumberOptional

Cap on participants per group conversation.

Default256
maxMessageLengthnumberOptional

Maximum message length in characters, enforced on send and edit.

Default8000
editingEnabled / deletionEnabledbooleanOptional

Let senders edit or delete their own messages. Deletion is soft — content is cleared and deletedAt set, so the conversation history stays coherent.

Defaulttrue
typingIndicators / readReceiptsbooleanOptional

Broadcast 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.

Defaulttrue
presencebooleanOptional

Reserved 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.

Defaulttrue
moderatorRolesstring[]Optional

Roles 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.

Default[]
realtimeTopicPrefixstringOptional

Topic prefix for the realtime events listed below.

Default"chat"
messagePageSizenumberOptional

Messages per history page. The messages endpoint cursor-paginates newest-first with limit clamped to 1–100.

Default50
attachments{ enabled?, maxPerMessage?, maxFileSizeBytes?, allowedMimeTypes? }Optional

File 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 /conversationslist

Your conversations, ordered by last activity. limit (default 30, max 100) + offset.

POST /conversationscreate

Direct ({ 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 one

Fetch a single conversation you belong to, including participants, your participant record and unreadCount.

GET /conversations/:id/messageshistory

Newest-first cursor pagination: limit + before=<messageId> returns { messages, hasMore }.

POST /conversations/:id/messagessend

Send 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 · /typingsignals

Mark 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 / delete

Edit 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/:userIdmembership

Add 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.

ChatPanelcomponent

The full messenger surface. Pairs with useChat / useChatStore and the NewConversationModal; styling via chatPanelTheme / extendChatPanelTheme.

usePubSubhook

The realtime hook the panel uses under the hood — subscribe to the chat topics yourself if you build a custom UI.

Related sections