Stream Chat
"Stream (GetStream): chat SDK, channels, threads, reactions, typing indicators, moderation, React components"
Stream Chat provides a complete real-time chat infrastructure with client SDKs and pre-built UI components. The architecture separates server-side operations (creating users, managing channels, moderation) from client-side operations (sending messages, subscribing to events). Server-side calls use an API key and secret, while client-side calls use tokens generated server-side. Design your chat around Stream's channel model: channels hold messages, members, and state. Use Stream's React components as a foundation and customize through theming and component overrides rather than building from scratch. Let Stream handle the real-time transport, message storage, and delivery — focus your code on business logic and user experience.
## Key Points
- Generate user tokens server-side only; never expose your API secret to the client
- Use `upsertUser` to sync your user database with Stream on login — it creates or updates as needed
- Set channel types appropriately: "messaging" for 1:1/group DMs, "team" for persistent workspaces, "livestream" for large broadcast channels
- Enable presence tracking selectively — it adds WebSocket overhead, so disable on channels that do not need online status
- Use `channel.watch()` to subscribe to real-time events and load initial state in one call
- Implement pagination for message history using `channel.query({ messages: { limit: 25, id_lt: lastMessageId } })`
- Customize the UI through Stream's theming CSS variables before resorting to custom components
- Handle disconnection and reconnection gracefully — the SDK auto-reconnects, but update your UI state accordingly
- **Using the server secret on the client** — This grants full admin access; always generate tokens server-side and pass them to the client
- **Creating a new StreamChat instance per component** — Use `StreamChat.getInstance()` to share a single connection; multiple instances cause duplicate WebSocket connections
- **Polling for new messages** — Stream pushes events in real time; polling wastes bandwidth and adds latency
- **Storing messages in your own database** — Stream persists messages; duplicating storage creates sync issues and added complexityskilldb get messaging-services-skills/Stream ChatFull skill: 366 linesStream Chat SDK
Core Philosophy
Stream Chat provides a complete real-time chat infrastructure with client SDKs and pre-built UI components. The architecture separates server-side operations (creating users, managing channels, moderation) from client-side operations (sending messages, subscribing to events). Server-side calls use an API key and secret, while client-side calls use tokens generated server-side. Design your chat around Stream's channel model: channels hold messages, members, and state. Use Stream's React components as a foundation and customize through theming and component overrides rather than building from scratch. Let Stream handle the real-time transport, message storage, and delivery — focus your code on business logic and user experience.
Setup
Server-Side Configuration
import { StreamChat } from "stream-chat";
// Server client — has full admin access
const serverClient = StreamChat.getInstance(
process.env.STREAM_API_KEY!,
process.env.STREAM_API_SECRET!
);
// Generate user tokens server-side
function generateUserToken(userId: string): string {
return serverClient.createToken(userId);
}
// Upsert a user (create or update)
async function upsertUser(userId: string, name: string, imageUrl?: string) {
await serverClient.upsertUser({
id: userId,
name,
image: imageUrl,
role: "user",
});
}
Client-Side React Setup
import { StreamChat } from "stream-chat";
import {
Chat,
Channel,
ChannelHeader,
MessageList,
MessageInput,
Thread,
Window,
} from "stream-chat-react";
import "stream-chat-react/dist/css/v2/index.css";
const apiKey = process.env.NEXT_PUBLIC_STREAM_API_KEY!;
function ChatApp({ userId, userToken, userName }: {
userId: string;
userToken: string;
userName: string;
}) {
const [client, setClient] = useState<StreamChat | null>(null);
useEffect(() => {
const chatClient = StreamChat.getInstance(apiKey);
chatClient.connectUser(
{ id: userId, name: userName },
userToken
).then(() => {
setClient(chatClient);
});
return () => {
chatClient.disconnectUser();
};
}, [userId, userToken, userName]);
if (!client) return <div>Loading chat...</div>;
return (
<Chat client={client} theme="str-chat__theme-light">
<ChannelListContainer />
<ChannelContainer />
</Chat>
);
}
Key Techniques
Channel Management
// Server-side: create a channel
async function createChannel(
type: "messaging" | "team" | "livestream",
id: string,
creatorId: string,
members: string[],
name?: string
) {
const channel = serverClient.channel(type, id, {
name: name || id,
members,
created_by_id: creatorId,
});
await channel.create();
return channel;
}
// Create a direct message channel between two users
async function createDmChannel(userId1: string, userId2: string) {
const channel = serverClient.channel("messaging", {
members: [userId1, userId2],
});
await channel.create();
return channel;
}
// Update channel data
async function updateChannel(channelType: string, channelId: string, updates: Record<string, unknown>) {
const channel = serverClient.channel(channelType, channelId);
await channel.update({
...updates,
updated_by_id: "admin",
});
}
// Add or remove members
async function manageMembers(channelType: string, channelId: string) {
const channel = serverClient.channel(channelType, channelId);
await channel.addMembers(["user-3", "user-4"]);
await channel.removeMembers(["user-2"]);
await channel.addModerators(["user-3"]);
}
React Channel List and Messages
import {
ChannelList,
ChannelPreviewMessenger,
useChatContext,
} from "stream-chat-react";
function ChannelListContainer() {
const { client } = useChatContext();
const filters = {
type: "messaging",
members: { $in: [client.userID!] },
};
const sort = { last_message_at: -1 as const };
const options = { state: true, presence: true, limit: 20 };
return (
<ChannelList
filters={filters}
sort={sort}
options={options}
Preview={ChannelPreviewMessenger}
showChannelSearch
/>
);
}
function ChannelContainer() {
return (
<Channel>
<Window>
<ChannelHeader />
<MessageList />
<MessageInput />
</Window>
<Thread />
</Channel>
);
}
Sending Messages and Attachments
// Client-side message sending
async function sendMessage(channel: ReturnType<StreamChat["channel"]>, text: string) {
await channel.sendMessage({ text });
}
// Send with attachments
async function sendWithAttachment(channel: ReturnType<StreamChat["channel"]>) {
await channel.sendMessage({
text: "Check out this file",
attachments: [
{
type: "image",
asset_url: "https://example.com/photo.jpg",
thumb_url: "https://example.com/photo-thumb.jpg",
title: "Photo",
},
],
});
}
// Reply in a thread
async function replyInThread(
channel: ReturnType<StreamChat["channel"]>,
parentMessageId: string,
text: string
) {
await channel.sendMessage({
text,
parent_id: parentMessageId,
show_in_channel: false, // true to also show in main channel
});
}
Reactions and Typing Indicators
// Send a reaction
async function addReaction(
channel: ReturnType<StreamChat["channel"]>,
messageId: string,
reactionType: string
) {
await channel.sendReaction(messageId, { type: reactionType });
}
// Remove a reaction
async function removeReaction(
channel: ReturnType<StreamChat["channel"]>,
messageId: string,
reactionType: string
) {
await channel.deleteReaction(messageId, reactionType);
}
// Typing indicators — call on keystroke (SDK debounces internally)
async function handleTyping(channel: ReturnType<StreamChat["channel"]>) {
await channel.keystroke();
}
// Stop typing indicator
async function handleStopTyping(channel: ReturnType<StreamChat["channel"]>) {
await channel.stopTyping();
}
Event Handling
function useChatEvents(channel: ReturnType<StreamChat["channel"]>) {
useEffect(() => {
const handleNewMessage = channel.on("message.new", (event) => {
if (event.message && event.user?.id !== channel.state?.membership?.user?.id) {
showNotification(`New message from ${event.user?.name}: ${event.message.text}`);
}
});
const handleTyping = channel.on("typing.start", (event) => {
console.log(`${event.user?.name} is typing...`);
});
const handlePresence = channel.on("user.presence.changed", (event) => {
console.log(`${event.user?.name} is now ${event.user?.online ? "online" : "offline"}`);
});
return () => {
handleNewMessage.unsubscribe();
handleTyping.unsubscribe();
handlePresence.unsubscribe();
};
}, [channel]);
}
Moderation
// Server-side moderation functions
async function moderateMessage(messageId: string, action: "flag" | "ban" | "delete") {
if (action === "delete") {
await serverClient.deleteMessage(messageId, true); // hard delete
}
if (action === "flag") {
await serverClient.flagMessage(messageId);
}
}
async function banUser(userId: string, channelType?: string, channelId?: string, reason?: string) {
await serverClient.banUser(userId, {
banned_by_id: "admin",
reason: reason || "Violation of community guidelines",
timeout: 60, // minutes; omit for permanent ban
...(channelType && channelId ? { type: channelType, id: channelId } : {}),
});
}
async function muteUser(userId: string, adminId: string) {
await serverClient.muteUser(userId, adminId);
}
// Auto-moderation via channel config
async function setupAutoModeration(channelType: string) {
await serverClient.updateChannelType(channelType, {
automod: "AI",
automod_behavior: "flag",
blocklist_behavior: "block",
});
}
Custom Message Component
import { MessageSimple, useMessageContext } from "stream-chat-react";
function CustomMessage() {
const { message, isMyMessage } = useMessageContext();
return (
<div className={`custom-message ${isMyMessage() ? "mine" : "theirs"}`}>
<div className="message-author">{message.user?.name}</div>
<MessageSimple />
<div className="message-time">
{new Date(message.created_at as string).toLocaleTimeString()}
</div>
</div>
);
}
// Use in Channel component
<Channel Message={CustomMessage}>
<Window>
<MessageList />
<MessageInput />
</Window>
</Channel>
Best Practices
- Generate user tokens server-side only; never expose your API secret to the client
- Use
upsertUserto sync your user database with Stream on login — it creates or updates as needed - Set channel types appropriately: "messaging" for 1:1/group DMs, "team" for persistent workspaces, "livestream" for large broadcast channels
- Enable presence tracking selectively — it adds WebSocket overhead, so disable on channels that do not need online status
- Use
channel.watch()to subscribe to real-time events and load initial state in one call - Implement pagination for message history using
channel.query({ messages: { limit: 25, id_lt: lastMessageId } }) - Customize the UI through Stream's theming CSS variables before resorting to custom components
- Handle disconnection and reconnection gracefully — the SDK auto-reconnects, but update your UI state accordingly
Anti-Patterns
- Using the server secret on the client — This grants full admin access; always generate tokens server-side and pass them to the client
- Creating a new StreamChat instance per component — Use
StreamChat.getInstance()to share a single connection; multiple instances cause duplicate WebSocket connections - Polling for new messages — Stream pushes events in real time; polling wastes bandwidth and adds latency
- Storing messages in your own database — Stream persists messages; duplicating storage creates sync issues and added complexity
- Ignoring channel member limits — Channels of type "messaging" have default member limits; use "livestream" for large audiences
- Blocking the UI on channel creation — Create channels asynchronously and show optimistic UI; waiting for the server round-trip feels sluggish
- Not cleaning up event listeners — Unsubscribe from channel events when components unmount to prevent memory leaks and stale callbacks
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"
Sendbird
"Sendbird: chat SDK, group channels, open channels, messages, file sharing, moderation, UIKit components"