Guides

Conversations and messages

Direct, group, channel, and forum conversations. messages.send commits in PostgreSQL. History uses cursors, not OFFSET.

Conversations

  • createDirect({ creatorId, userIds: [a, b] }) is idempotent for a pair. Direct chats do not accept a retention write.
  • createGroup({ creatorId, title, userIds }) assigns owner to the creator.
  • createChannel / createForum use the same membership model. Discovery and ranking stay in the application.
  • Roles: owner, admin, moderator, member.

Public Conversation includes nested resolved .retention. Drive UI from that view. Staff who may change modes use conversations.updateRetentionPolicy. See retention.

Messages

messages.send validates, authorizes, rate-limits, then writes the message and message_outbox in one PostgreSQL transaction (plus push_intents when push is enabled and the event is a visible message.created). The call returns after commit. Centrifugo and push-provider success are not required.

Every send needs a clientMessageId. Retries with the same sender and key return the original row.

Supported types: text, image, audio, video, file, sticker, gif. system is reserved.

metadata.mentions, when present, must be an array of strings. Unknown, duplicate, self, and over-cap IDs are dropped rather than rejected so send cannot probe whether a user id exists.

History and sync

History uses opaque cursors (before / after), not SQL OFFSET. Forums can pass rootsOnly: true. Nested replies load with messages.thread({ threadRootId }).

After reconnect, call messages.sync({ afterSequence }). Deduplicate by message id. Do not poll. Reload the current history page as well so in-place expiry and moderation transitions are not skipped.

Receipts

markDelivered and markRead are monotonic. Read implies delivered. Senders cannot receipt their own messages. Invisible (held, blocked, expired, or deleted) messages cannot be receipted. For after_read, the first recipient markRead arms expiresAt.