Skip to main content
Technology & EngineeringMessaging Services366 lines

Stream Chat

"Stream (GetStream): chat SDK, channels, threads, reactions, typing indicators, moderation, React components"

Quick Summary18 lines
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 complexity
skilldb get messaging-services-skills/Stream ChatFull skill: 366 lines
Paste into your CLAUDE.md or agent config

Stream 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 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

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

Get CLI access →