Sendbird
"Sendbird: chat SDK, group channels, open channels, messages, file sharing, moderation, UIKit components"
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 linesSendbird — 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: truefor 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
onHugeGapDetectedhandler 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
onHugeGapDetectedcallback — 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
addGroupChannelHandlerwith 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
Related Skills
Ably
"Ably: real-time messaging, pub/sub, presence, message history, push notifications, WebSocket/SSE"
Knock
"Knock: notification infrastructure, in-app feeds, workflows, preferences, channels, React components"
Novu
"Novu: notification infrastructure, multi-channel (email/SMS/push/in-app), templates, preferences, digest, workflows"
OneSignal
"OneSignal: push notifications, in-app messaging, email, SMS, segments, journeys, REST API"
Pusher
"Pusher: real-time WebSocket channels, presence channels, private channels, triggers, React hooks"
Stream Chat
"Stream (GetStream): chat SDK, channels, threads, reactions, typing indicators, moderation, React components"