Skip to main content
Technology & EngineeringMessaging Services321 lines

Sendbird

"Sendbird: chat SDK, group channels, open channels, messages, file sharing, moderation, UIKit components"

Quick Summary26 lines
You are an expert in integrating Sendbird for in-app messaging and chat functionality.

## Key Points

- Use `isDistinct: true` for 1-on-1 direct messages so the SDK reuses the existing channel between the same two users instead of creating duplicates
- Use message collections instead of manual query pagination; collections handle local caching, real-time sync, and gap detection automatically
- Issue access tokens via the Platform API and require them in production; token-free connections should only be used during development
- Call `channel.markAsRead()` when the user views a channel to keep unread counts accurate across all connected devices
- Dispose message and channel collections when navigating away from the chat view to free memory and stop unnecessary event processing
- Use the `onHugeGapDetected` handler to dispose and recreate the message collection, which handles cases where the user was offline for an extended period
- **Not calling `sb.disconnect()` on logout** — The WebSocket stays open and the user appears online; always disconnect during sign-out or session expiry
- **Creating non-distinct channels for DMs** — Without `isDistinct: true`, each conversation attempt creates a new channel, fragmenting message history between the same users
- **Fetching messages with raw queries instead of collections** — Manual pagination misses real-time updates and local caching; message collections handle both seamlessly
- **Exposing the Platform API token on the client** — The API token grants full admin access; it must only be used server-side, never bundled in client code

## Quick Example

```bash
# Core JavaScript SDK
npm install @sendbird/chat

# React UIKit (includes core SDK)
npm install @sendbird/uikit-react
```
skilldb get messaging-services-skills/SendbirdFull skill: 321 lines
Paste into your CLAUDE.md or agent config

Sendbird — Messaging Integration

You are an expert in integrating Sendbird for in-app messaging and chat functionality.

Core Philosophy

Overview

Sendbird is a chat and messaging platform providing SDKs for real-time one-on-one, group, and open channel communication. It handles message persistence, delivery receipts, typing indicators, online presence, file sharing, and moderation. The platform offers both a low-level Chat SDK for full control and a UIKit with pre-built UI components for rapid development. Server-side operations use the Platform API with an API token for user management, channel administration, and moderation.

Setup & Configuration

Install SDKs

# Core JavaScript SDK
npm install @sendbird/chat

# React UIKit (includes core SDK)
npm install @sendbird/uikit-react

Initialize the Chat SDK

import SendbirdChat, { SendbirdGroupChat } from "@sendbird/chat";
import { GroupChannelModule } from "@sendbird/chat/groupChannel";
import { OpenChannelModule } from "@sendbird/chat/openChannel";

const sb = SendbirdChat.init({
  appId: process.env.NEXT_PUBLIC_SENDBIRD_APP_ID!,
  modules: [new GroupChannelModule(), new OpenChannelModule()],
}) as SendbirdGroupChat;

// Connect a user
async function connectUser(userId: string, accessToken?: string) {
  const user = await sb.connect(userId, accessToken);
  return user;
}

// Disconnect on logout
async function disconnectUser() {
  await sb.disconnect();
}

UIKit React Setup

import SendbirdProvider from "@sendbird/uikit-react/SendbirdProvider";
import ChannelList from "@sendbird/uikit-react/ChannelList";
import Channel from "@sendbird/uikit-react/Channel";
import "@sendbird/uikit-react/dist/index.css";
import { useState } from "react";

function ChatApp({ userId, accessToken }: { userId: string; accessToken?: string }) {
  const [channelUrl, setChannelUrl] = useState<string>("");

  return (
    <SendbirdProvider
      appId={process.env.NEXT_PUBLIC_SENDBIRD_APP_ID!}
      userId={userId}
      accessToken={accessToken}
    >
      <div style={{ display: "flex", height: "100vh" }}>
        <div style={{ width: 320 }}>
          <ChannelList onChannelSelect={(channel) => {
            if (channel) setChannelUrl(channel.url);
          }} />
        </div>
        <div style={{ flex: 1 }}>
          {channelUrl && <Channel channelUrl={channelUrl} />}
        </div>
      </div>
    </SendbirdProvider>
  );
}

Core Patterns

Group Channel Management

import { GroupChannelModule } from "@sendbird/chat/groupChannel";

// Create a group channel (private by default)
async function createGroupChannel(userIds: string[], name: string) {
  const params = {
    invitedUserIds: userIds,
    name,
    isDistinct: false, // true reuses existing channel with same members
    operatorUserIds: [userIds[0]], // first user is operator
  };
  const channel = await sb.groupChannel.createChannel(params);
  return channel;
}

// Create a 1-on-1 distinct channel (reuses if exists)
async function createDirectMessage(userId1: string, userId2: string) {
  const params = {
    invitedUserIds: [userId1, userId2],
    isDistinct: true,
  };
  const channel = await sb.groupChannel.createChannel(params);
  return channel;
}

// Fetch a channel by URL
async function getChannel(channelUrl: string) {
  const channel = await sb.groupChannel.getChannel(channelUrl);
  return channel;
}

// List channels for the current user
async function listMyChannels() {
  const collection = sb.groupChannel.createGroupChannelCollection({
    filter: {
      includeEmpty: false,
    },
    order: "latest_last_message",
    limit: 20,
  });

  const channels = await collection.loadMore();
  return channels;
}

Sending and Loading Messages

// Send a text message
async function sendTextMessage(channelUrl: string, text: string) {
  const channel = await sb.groupChannel.getChannel(channelUrl);
  const params = { message: text };
  const message = await channel.sendUserMessage(params);
  return message;
}

// Send a file message
async function sendFileMessage(channelUrl: string, file: File) {
  const channel = await sb.groupChannel.getChannel(channelUrl);
  const params = {
    file,
    fileName: file.name,
    fileSize: file.size,
    mimeType: file.type,
  };
  const message = await channel.sendFileMessage(params);
  return message;
}

// Load previous messages with a message collection
async function loadMessages(channelUrl: string) {
  const channel = await sb.groupChannel.getChannel(channelUrl);

  const collection = channel.createMessageCollection({
    filter: {},
    startingPoint: Date.now(),
    limit: 30,
  });

  // Set handler for real-time updates
  collection.setMessageCollectionHandler({
    onMessagesAdded: (context, channel, messages) => {
      // Append new messages to UI
    },
    onMessagesUpdated: (context, channel, messages) => {
      // Update edited messages in UI
    },
    onMessagesDeleted: (context, channel, messageIds) => {
      // Remove deleted messages from UI
    },
    onChannelUpdated: (context, channel) => {},
    onChannelDeleted: (context, channelUrl) => {},
    onHugeGapDetected: () => {
      // Dispose and recreate the collection
    },
  });

  // Load initial messages
  const messages = await collection.initialize("cache_and_replace_by_api");
  return { collection, messages };
}

Typing Indicators and Read Receipts

// Start typing indicator
async function startTyping(channelUrl: string) {
  const channel = await sb.groupChannel.getChannel(channelUrl);
  channel.startTyping();
}

// Stop typing indicator
async function stopTyping(channelUrl: string) {
  const channel = await sb.groupChannel.getChannel(channelUrl);
  channel.endTyping();
}

// Mark messages as read
async function markAsRead(channelUrl: string) {
  const channel = await sb.groupChannel.getChannel(channelUrl);
  await channel.markAsRead();
}

// Listen for typing status changes
function onTypingStatusChange(channelUrl: string, callback: (members: string[]) => void) {
  const handler = {
    onTypingStatusUpdated: (channel: { url: string; getTypingUsers: () => Array<{ nickname: string }> }) => {
      if (channel.url === channelUrl) {
        const typingMembers = channel.getTypingUsers().map((u) => u.nickname);
        callback(typingMembers);
      }
    },
  };
  const handlerKey = `typing_${channelUrl}`;
  sb.groupChannel.addGroupChannelHandler(handlerKey, handler);
  return () => sb.groupChannel.removeGroupChannelHandler(handlerKey);
}

Platform API (Server-Side)

// Server-side: use the Platform API for admin operations
const SENDBIRD_API_URL = `https://api-${process.env.SENDBIRD_APP_ID}.sendbird.com/v3`;
const headers = {
  "Content-Type": "application/json",
  "Api-Token": process.env.SENDBIRD_API_TOKEN!,
};

// Create a user
async function createUser(userId: string, nickname: string, profileUrl?: string) {
  const response = await fetch(`${SENDBIRD_API_URL}/users`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      user_id: userId,
      nickname,
      profile_url: profileUrl || "",
      issue_access_token: true,
    }),
  });
  return response.json();
}

// Ban a user from a channel
async function banUser(channelUrl: string, userId: string, seconds?: number) {
  await fetch(`${SENDBIRD_API_URL}/group_channels/${channelUrl}/ban`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      user_id: userId,
      seconds: seconds || -1, // -1 for permanent
      description: "Violation of community guidelines",
    }),
  });
}

// Mute a user in a channel
async function muteUser(channelUrl: string, userId: string, seconds?: number) {
  await fetch(`${SENDBIRD_API_URL}/group_channels/${channelUrl}/mute`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      user_id: userId,
      seconds: seconds || -1,
    }),
  });
}

// Send an admin message
async function sendAdminMessage(channelUrl: string, message: string) {
  await fetch(`${SENDBIRD_API_URL}/group_channels/${channelUrl}/messages`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      message_type: "ADMM",
      message,
    }),
  });
}

Best Practices

  • Use isDistinct: true for 1-on-1 direct messages so the SDK reuses the existing channel between the same two users instead of creating duplicates
  • Use message collections instead of manual query pagination; collections handle local caching, real-time sync, and gap detection automatically
  • Issue access tokens via the Platform API and require them in production; token-free connections should only be used during development
  • Call channel.markAsRead() when the user views a channel to keep unread counts accurate across all connected devices
  • Dispose message and channel collections when navigating away from the chat view to free memory and stop unnecessary event processing
  • Use the onHugeGapDetected handler to dispose and recreate the message collection, which handles cases where the user was offline for an extended period

Common Pitfalls

  • Not calling sb.disconnect() on logout — The WebSocket stays open and the user appears online; always disconnect during sign-out or session expiry
  • Creating non-distinct channels for DMs — Without isDistinct: true, each conversation attempt creates a new channel, fragmenting message history between the same users
  • Fetching messages with raw queries instead of collections — Manual pagination misses real-time updates and local caching; message collections handle both seamlessly
  • Exposing the Platform API token on the client — The API token grants full admin access; it must only be used server-side, never bundled in client code
  • Ignoring the onHugeGapDetected callback — When the gap between cached and server messages is too large, the collection cannot sync incrementally; failing to recreate it leaves the UI with stale or incomplete data
  • Registering channel handlers without removing them — Each call to addGroupChannelHandler with the same key replaces the previous one, but using unique keys without cleanup accumulates handlers and causes duplicate event processing

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add messaging-services-skills

Get CLI access →