> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# Messaging

> Build a chat surface with channels, direct messages, reactions, and read receipts.

DeepSpace ships a complete messaging layer as drop-in schemas plus a small set of React hooks. Wire the schemas into your app's `RecordRoom`, mount the providers (already done in the scaffold), and compose a chat surface from `useChannels`, `useMessages`, and `useReactions`.

This guide walks the end-to-end flow. For the exhaustive hook signatures, options, and return shapes, see the [messaging reference](/sdk-reference/client/messaging).

## Add the schemas

Pick the schemas your UI actually uses. `CHANNELS_SCHEMA` + `MESSAGES_SCHEMA` is the minimum; the rest are additive.

```ts theme={null}
// src/schemas.ts
import type { CollectionSchema } from 'deepspace/worker'
import {
  CHANNELS_SCHEMA,
  MESSAGES_SCHEMA,
  REACTIONS_SCHEMA,
  CHANNEL_MEMBERS_SCHEMA,
  READ_RECEIPTS_SCHEMA,
} from 'deepspace/worker'

import { usersSchema } from './schemas/users-schema'
import { settingsSchema } from './schemas/admin-schema'

export const schemas: CollectionSchema[] = [
  usersSchema,
  settingsSchema,
  CHANNELS_SCHEMA,
  MESSAGES_SCHEMA,
  REACTIONS_SCHEMA,
  CHANNEL_MEMBERS_SCHEMA,
  READ_RECEIPTS_SCHEMA,
]
```

The drop-in schemas include sensible [RBAC](/concepts/permissions) defaults: signed-in users can read channels and post messages, and only the author can edit or delete a message. Private-channel gating is an app-level concern - enforce it with `useChannelMembers` (see [Channel membership](#channel-membership)).

## Build the chat surface

A minimal chat surface. `useChannels` lists the sidebar, `useMessages` drives the transcript, and `useReactions` gives every message a toggleable emoji. Identity (author) is derived from the verified JWT - you never pass an `authorId`.

```tsx theme={null}
import { useState } from 'react'
import {
  useChannels,
  useMessages,
  useReactions,
  formatMessageTime,
} from 'deepspace'

export default function ChatApp() {
  const { channels, status } = useChannels()
  const [activeId, setActiveId] = useState<string | null>(null)

  if (status === 'loading') return <SkeletonList />

  return (
    <div className="grid grid-cols-[240px_1fr] h-screen">
      <aside>
        {channels.map((ch) => (
          <button key={ch.recordId} onClick={() => setActiveId(ch.recordId)}>
            #{ch.data.name}
          </button>
        ))}
      </aside>
      <main>{activeId && <ChannelView channelId={activeId} />}</main>
    </div>
  )
}

function ChannelView({ channelId }: { channelId: string }) {
  const { messages, send } = useMessages(channelId)
  const { toggle, getReactionsForMessage } = useReactions(channelId)
  const [draft, setDraft] = useState('')

  return (
    <>
      <ul>
        {messages.map((m) => (
          <li key={m.recordId}>
            <span>{m.data.authorId}</span>
            <span>{formatMessageTime(m.createdAt)}</span>
            <p>{m.data.content}</p>
            {getReactionsForMessage(m.recordId).map((r) => (
              <button key={r.emoji} onClick={() => toggle(m.recordId, r.emoji)}>
                {r.emoji} {r.count}
              </button>
            ))}
            <button onClick={() => toggle(m.recordId, '❤️')}>+❤️</button>
          </li>
        ))}
      </ul>
      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (!draft.trim()) return
          send(draft)
          setDraft('')
        }}
      >
        <input value={draft} onChange={(e) => setDraft(e.target.value)} />
        <button>Send</button>
      </form>
    </>
  )
}
```

Sends, edits, reactions, and deletes all dispatch optimistically and reconcile through the room. Prefer `softDelete(messageId)` over `remove(messageId)` for user-initiated deletes - it keeps thread replies and read receipts coherent. See [`useChannels`](/sdk-reference/client/messaging#usechannels), [`useMessages` options](/sdk-reference/client/messaging#usemessages-channelid-options), and [`useReactions`](/sdk-reference/client/messaging#usereactions-channelid) for the full API surface.

## Create a channel

Channel `type` is one of `'public'`, `'private'`, or `'dm'`.

```tsx theme={null}
const { create } = useChannels()

const channelId = await create({
  name: 'general',
  type: 'public',
  description: 'Company-wide announcements',
})
```

<Warning>
  **`type` is required.** Passing only `{ name }` produces a channel with `type: undefined`, which silently breaks downstream queries.
</Warning>

## Direct messages

DMs are just channels with `type: 'dm'`. By convention, encode the participant pair in the channel `name` so you can dedupe locally before calling `create`.

```tsx theme={null}
const { create } = useChannels()

async function startDM(otherUserId: string, currentUserId: string) {
  const channelId = await create({
    name: `dm:${[currentUserId, otherUserId].sort().join(':')}`,
    type: 'dm',
  })
  navigate(`/messages/${channelId}`)
}
```

The same `useMessages(channelId)` and `useReactions(channelId)` hooks drive the DM thread - no separate API. For cross-app DMs that live on a dedicated Durable Object scope, use [`useConversation`](/sdk-reference/client/messaging#useconversation-options) instead.

## Channel membership

For private channels, gate posting on membership. `isMember` is a derived boolean for the current signed-in user.

```tsx theme={null}
import { useChannelMembers } from 'deepspace'

function JoinGate({ channelId }: { channelId: string }) {
  const { isMember, join } = useChannelMembers(channelId)
  if (!isMember) return <button onClick={() => join()}>Join channel</button>
  return <MessageComposer channelId={channelId} />
}
```

See [`useChannelMembers`](/sdk-reference/client/messaging#usechannelmembers-channelid) for the full membership surface.

## Read receipts

Stamp the channel as read whenever the user has it open, then derive unread counts from the message list.

```tsx theme={null}
import { useEffect } from 'react'
import { useMessages, useReadReceipts } from 'deepspace'

function ChannelView({ channelId }: { channelId: string }) {
  const { messages } = useMessages(channelId)
  const { markAsRead, getUnreadCount } = useReadReceipts()

  useEffect(() => {
    markAsRead(channelId)
  }, [channelId, messages.length, markAsRead])

  const unread = getUnreadCount(channelId, messages)
  // ...
}
```

`markAsRead` writes the current timestamp; `getUnreadCount` counts messages that landed after it. The receipt is per-user, scoped by the verified JWT. Full signatures live in [`useReadReceipts`](/sdk-reference/client/messaging#usereadreceipts).

## Next steps

* [Presence and cursors](/guides/presence-and-cursors) - typing indicators and online status.
* [Permissions](/concepts/permissions) - making channels private to specific roles.
* [Messaging reference](/sdk-reference/client/messaging) - full API for channels, messages, reactions, members, and read receipts.
