Conduit
Conduit
Docsllms.txtHostingGitHubIntroduction

Getting Started

OverviewInstall ConduitMCP SetupYour First AppStart with AI

Learn

ArchitectureClient vs Admin APIConfiguration

Modules

OverviewAuthenticationAuthorizationDatabaseStorageCommunicationsChatRouterFunctions

Guides

Next.js IntegrationReBAC Team ScopingGitOps State Export

Deployment

Deployment OverviewDocker ComposeKubernetes and HelmLocal from SourceContainer Images

Reference

CLI ReferenceClient APIAdmin APIEnvironment VariablesMCP Tools

Resources

Migration v0.16 → v0.17Legacy DocumentationChangelogFAQGlossaryContributing

Chat

Realtime rooms and messages over REST and Socket.io.

Apps need persisted chat history, live delivery, and controlled membership — without building a separate messaging stack. The chat module stores rooms and messages in the database, exposes CRUD on the Client API, and pushes realtime events over Socket.io. Custom modules provision rooms and system messages via grpcSdk.chat; operators manage data through the Admin API or MCP.

Use cases

In-app messaging

Direct and group chat with REST history + live Socket.io updates

Support tickets

Custom module creates rooms via grpcSdk.chat; users join via Client API

Opt-in room membership

explicit_room_joins sends invitations; users accept before joining

File attachments

Upload via storage, reference file IDs in message payload

Read receipts & typing

Socket events for presence and message state

Capabilities

  • Room CRUD (Client + Admin API)
  • Message history & single-message fetch
  • Socket.io realtime
  • Typing indicators
  • Read receipts (batched flush)
  • Message edit/delete (config-gated)
  • File & multimedia attachments
  • System messages (grpcSdk)
  • Explicit room joins & invitations
  • Invitation email & push (optional)
  • Audit mode & empty-room cleanup

Example: Create room and send a message

Walkthrough

  1. Authenticate user and obtain accessToken
  2. POST /chat/rooms with roomName and users[] participant IDs (creator is added automatically — do not include your own id in users)
  3. Connect Socket.io to SOCKET_BASE_URL/chat/ with path /realtime and Bearer header
  4. On connect the server joins all rooms the user belongs to; emit connectToRoom when opening a specific room UI
  5. Load history via GET /chat/messages?roomId=ROOM_ID&skip=0&limit=20
  6. Send live messages via socket emit message with roomId and payload
Create room
curl -X POST http://localhost:3000/chat/rooms \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"roomName":"Support #42","users":["507f1f77bcf86cd799439011","507f191e810c19729de860ea"]}'
Message history
curl "http://localhost:3000/chat/messages?roomId=ROOM_ID&skip=0&limit=20" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"

How it works

Surfaces

SurfaceBasePurpose
RESTCLIENT_BASE_URL/chat/...Room CRUD, message history, edit/delete, invitations
Socket.ioNamespace /chat/ on socket portLive messages, typing, read receipts, room membership events
gRPCgrpcSdk.chatCustom modules create rooms, send system messages, delete rooms
Admin APIADMIN_BASE_URL/chat/...Operator room/message/invitation management

Default ports: REST 3000, WebSockets 3001 (override via CLIENT_HTTP_PORT / CLIENT_SOCKET_PORT on router).

Rooms

A ChatRoom has a name, creator, participants[], and a participantsLog audit trail (create, join, leave, add, remove).

ModeCreate roomAdd users
Default (explicit_room_joins.enabled: false)Creator + all users[] become participants immediately; sockets receive join-room / room-joinedUsers are added directly; same socket events
Explicit joins (explicit_room_joins.enabled: true)Only the creator is a participant; invitation tokens are created for each user in users[]Sends invitations instead of adding members

PUT /chat/leave/:roomId removes the current user. When deleteEmptyRooms is true and one participant remains (or none), the room and its messages are removed — or soft-deleted when auditMode is true.

Messages

Messages support contentType: text, file, multimedia, typing, or system. File and multimedia types require a files[] array of storage file IDs. Typing events are ephemeral (not persisted). Optional nonce on send enables idempotent retries — duplicate nonces return the existing message to the sender only.

GET /chat/messages without roomId returns messages across all rooms the user participates in. With roomId, results are scoped and membership is validated. Pass populate (comma-separated relation names) to join sender or file metadata.

Edit (PATCH) and delete (DELETE) routes register only when allowMessageEdit / allowMessageDelete are enabled. Only the original sender can edit or delete their message. Successful mutations broadcast message-edited or message-deleted on the room socket.

Socket connection

import { io } from "socket.io-client";

const socket = io(`${SOCKET_BASE_URL}/chat/`, {
  path: "/realtime",
  extraHeaders: { authorization: `Bearer ${accessToken}` },
});
Client emitParamsBehavior
connect—Automatic on namespace connect; server joins all user rooms
connectToRoomroomIdJoin a specific room socket channel
messageroomId, string | objectSend text string or { contentType, content?, files?, nonce? }
messagesReadroomIdMark room messages read; broadcasts messagesRead
Server eventWhen
join-roomUser should join room channel(s)
room-joinedRoom membership confirmed ({ room, roomName })
leave-roomUser removed from room channel
messageNew or typing message
message-editedREST patch applied
message-deletedREST delete applied
messagesRead{ room, readBy }
message:errorPersist failed for optimistic send
room-deletedRoom removed (gRPC delete)

REST + socket split

ConcernChannel
Initial history, infinite scrollREST GET /chat/messages
Send, typing, read receiptsSocket emit
Edit / deleteREST PATCH/DELETE (broadcasts on socket)

Merge by message _id; dedupe socket events against REST pages. Read receipts are flushed to the database in a 500ms batch after messagesRead emits.

Attachments

Upload files via storage first. Send contentType: "file" or "multimedia" with files: [storageFileId] in the message payload. Serve binaries through a preview proxy — never presigned URLs in the client.

Explicit room joins & invitations

When explicit_room_joins.enabled is true, creating a room or calling PUT /chat/rooms/:roomId/addUsers creates InvitationToken documents instead of adding participants directly.

In-app flow (authenticated Client API):

MethodPathPurpose
GET/chat/invitations/receivedList invitations for current user
GET/chat/invitations/sentList invitations sent by current user
GET/chat/invitations/{accept|decline}/{id}Accept or decline by invitation document id
DELETE/chat/invitations/cancel/:idCancel a sent invitation (sender only)

On accept, the user is added to participants, invitation tokens for that room/receiver are cleared, and join-room / room-joined socket events fire.

Email / deep-link hook flow — when explicit_room_joins.send_email is true (requires communications email), invitation emails embed links:

GET {router.hostUrl}/hook/chat/invitations/accept/{token}
GET {router.hostUrl}/hook/chat/invitations/decline/{token}

The hook route is rate-limited and resolves the opaque token (not the document id).

  • Route auth: The hook uses optional auth middleware (authMiddleware?) — the URL itself is not a secret, but accepting or declining still requires a logged-in user before the invitation is applied.
  • Unauthenticated visitors: When explicit_room_joins.redirect.login_uri is set, unauthenticated users are redirected to your login page with redirectUri set back to the hook URL so they can complete the flow after signing in.
  • After accept/decline: Configure explicit_room_joins.redirect.accept_uri / decline_uri (supports {roomId}) for server-side redirects; otherwise the hook returns a plain-text result (Invitation accepted / Invitation declined).

Note: Email links have always used the /hook/chat/invitations/... path. Older releases registered the handler at /hook/invitations/... (without the chat segment), so those links 404'd until the route and email builder were aligned. Use /hook/chat/invitations/{accept|decline}/{token} as the canonical path.

Set router.hostUrl (patch_config_router) to your public Client API base URL so invitation links resolve correctly in production — defaults to http://localhost:3000 when unset.

With explicit_room_joins.send_notification enabled, push notifications are sent via communications when that module is serving.

Server-side provisioning

From custom modules or workers:

// Create room with all participants (bypasses invitation flow)
const room = await grpcSdk.chat!.createRoom({ name: "Order #99", participants: [userA, userB] });

// System message (optional persist: false for ephemeral)
await grpcSdk.chat!.sendMessage({
  roomId: room._id,
  userId: operatorId,
  message: "Ticket opened",
  messageType: "system",
});

await grpcSdk.chat!.deleteRoom({ _id: room._id });

gRPC createRoom always adds participants immediately — it does not honor explicit_room_joins. Use Client API room creation when invitation flow is required.

Configure

Patch via MCP patch_config_chat (?modules=chat):

KeyDefaultMeaning
activetrueEnable module
allowMessageEdittrueRegister PATCH /chat/messages/:messageId
allowMessageDeletetrueRegister DELETE /chat/messages/:messageId
deleteEmptyRoomsfalseDelete room when one or zero participants remain after leave
auditModefalseSoft-delete rooms/messages instead of hard delete
explicit_room_joins.enabledfalseRequire invitation acceptance before membership
explicit_room_joins.send_emailfalseEmail invitation links (needs communications email)
explicit_room_joins.send_notificationfalsePush on invite (needs communications push)
explicit_room_joins.redirect.login_uri""Login page for unauthenticated hook visitors (redirectUri query param)
explicit_room_joins.redirect.accept_uri""Post-accept redirect ({roomId} placeholder)
explicit_room_joins.redirect.decline_uri""Post-decline redirect

For invitation email links, also set router.hostUrl to your public API origin.

Domain workflows (e.g. auto-create room on order) typically use grpcSdk.chat from a custom module — see Functions.

Client API

MethodPathNotes
POST/chat/roomsBody: { roomName, users[] } → { roomId }
GET/chat/roomsQuery: skip, limit, populate
GET/chat/rooms/:idSingle room (membership required)
PUT/chat/rooms/:roomId/addUsersBody: { users[] }
PUT/chat/leave/:roomIdLeave room
GET/chat/messagesQuery: roomId?, skip, limit, populate
GET/chat/messages/:messageIdSingle message
PATCH/chat/messages/:messageIdBody: { newMessage } — if allowMessageEdit
DELETE/chat/messages/:messageIdIf allowMessageDelete
GET/chat/invitations/receivedIf explicit_room_joins.enabled
GET/chat/invitations/sentIf explicit_room_joins.enabled
GET/chat/invitations/{accept|decline}/:idIf explicit_room_joins.enabled
DELETE/chat/invitations/cancel/:idIf explicit_room_joins.enabled
GET/hook/chat/invitations/{accept|decline}/:tokenEmail deep link; optional auth + redirect config

Admin API

Operator routes on ADMIN_BASE_URL/chat/... (admin credentials):

MethodPathPurpose
GET/chat/roomsList/search rooms (search, users, deleted, skip, limit, sort, populate)
GET/chat/rooms/:idGet room
POST/chat/roomsCreate room (name, participants[], optional creator)
DELETE/chat/roomsDelete rooms by ids[] query
PUT/chat/rooms/:roomId/addAdd users (immediate, no invitation flow)
PUT/chat/room/:roomId/removeRemove users
GET/chat/invitations/:roomIdList pending invitations for room
DELETE/chat/invitations/:roomIdDelete invitations by invitations[] query
GET/chat/messagesQuery messages (roomId, senderUser, skip, limit, sort, populate)
DELETE/chat/messagesDelete messages by ids[] query

MCP

Enable with ?modules=chat in your MCP server URL for config and admin room/message management (patch_config_chat, room and message admin tools).

Next steps

  • Storage (attachments)
  • Communications (invite email/push)
  • Authentication
  • Router (socket port & hostUrl)
  • Next.js integration

Communications

Unified email, SMS, and push notifications (replaces legacy modules).

Router

Client API gateway — REST, GraphQL, and WebSockets.

On this page

Use casesCapabilitiesExample: Create room and send a messageHow it worksConfigureClient APIAdmin APIMCP