Skip to main content
Documentation

Messaging reference

Public channels, messages, reactions, members, and read receipts.

On this page

The messaging API is an app-owned, public-chat layer. Add the five messaging schemas to your RecordRoom, then import the hooks from deepspace.

import {
  useChannels,
  useMessages,
  useReactions,
  useChannelMembers,
  useReadReceipts,
} from 'deepspace'
ts

Each hook returns status: 'loading' | 'ready' | 'error' and error?: string with its records. The SDK does not include a global conversation or directory service.

useChannels()#

function useChannels(): {
  channels: RecordData<Channel>[]
  status: 'loading' | 'ready' | 'error'
  error?: string
  create: (input: { name: string; description?: string }) => Promise<string>
  update: (channelId: string, patch: Partial<Pick<Channel, 'name' | 'description'>>) => void
  archive: (channelId: string) => void
  remove: (channelId: string) => Promise<void>
}
ts

create always writes type: 'public'. update, archive, and remove are fire-and-forget. The remove promise resolves after the client sends the request, before the room answers; handle a room refusal with RecordProvider.onWriteError.

const { create, archive } = useChannels()
const channelId = await create({ name: 'general', description: 'Announcements' })
archive(channelId)
tsx

useMessages(channelId, options?)#

function useMessages(
  channelId: string | undefined,
  options?: { parentMessageId?: string },
): {
  messages: RecordData<Message>[]
  status: 'loading' | 'ready' | 'error'
  error?: string
  send: (content: string, parentMessageId?: string) => Promise<string> | undefined
  edit: (messageId: string, newContent: string) => void
  softDelete: (messageId: string) => void
  remove: (messageId: string) => void
}
ts

send returns the new ID, or undefined without both a channel and signed-in user. edit, softDelete, and remove are fire-and-forget. Use softDelete for user-facing deletion so reply relationships remain intact. Pass options.parentMessageId to query one reply thread.

const { messages, send } = useMessages(channelId)
await send('Hello')
await send('A reply', parentMessageId)
tsx

useReactions(channelId)#

type GroupedReaction = {
  emoji: string
  count: number
  currentUserReacted: boolean
  userIds: string[]
}

function useReactions(channelId: string | undefined): {
  reactions: RecordData<Reaction>[]
  status: 'loading' | 'ready' | 'error'
  error?: string
  getReactionsForMessage: (messageId: string) => GroupedReaction[]
  toggle: (messageId: string, emoji: string) => void
}
ts

toggle is fire-and-forget. The schema's uniqueness constraint prevents duplicate (messageId, emoji, userId) rows.

useChannelMembers(channelId)#

function useChannelMembers(channelId: string | undefined): {
  members: RecordData<ChannelMember>[]
  status: 'loading' | 'ready' | 'error'
  error?: string
  join: () => Promise<void>
  leave: () => Promise<void>
  isMember: boolean
}
ts

join and leave use confirmed mutations. They resolve after acceptance and reject on failure. Membership is an opt-in signal for public channels; it does not restrict who can read messages.

useReadReceipts()#

function useReadReceipts(): {
  receipts: RecordData<ReadReceipt>[]
  status: 'loading' | 'ready' | 'error'
  error?: string
  markAsRead: (channelId: string) => void
  getUnreadCount: (channelId: string, messages: RecordData<Message>[]) => number
}
ts

markAsRead is fire-and-forget and stores the current timestamp. getUnreadCount compares message creation times with that timestamp.

Record types#

interface Channel {
  name: string
  description?: string
  type: 'public'
  createdBy: string
  archived: boolean
}

interface Message {
  channelId: string
  content: string
  authorId: string
  parentMessageId?: string
  edited: boolean
  editedAt?: string
  deleted?: boolean
}

interface Reaction {
  messageId: string
  channelId: string
  emoji: string
  userId: string
}

interface ChannelMember {
  channelId: string
  userId: string
  joinedAt: string
}

interface ReadReceipt {
  channelId: string
  userId: string
  lastReadAt: string
}
ts

Each value is wrapped in the standard record envelope (recordId, data, createdBy, createdAt, updatedAt).

See also#