---
title: The Complete Guide to Chat SDK
description: Chat SDK is a TypeScript library for building chat bots that work across Slack, Teams, Discord, Linear, and more from a single codebase. Learn how it works.
url: "https://vercel.com/kb/guide/the-complete-guide-to-chat-sdk"
published: 2026-04-29
last_updated: 2026-06-17
authors: Ben Sabic
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---

Chat SDK is the universal chat layer for building bots and agents.

With this open-source TypeScript SDK, you can build chat bots that work across multiple platforms from a single codebase. You write your bot logic once and deploy it to Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp, and other platforms (e.g., Resend).

In this guide, you'll learn:

- What Chat SDK is and the problem it solves
  
- The three core concepts: `Chat`, adapters, and state
  
- How to build your first bot in a few lines of code
  
- How to handle messages, streaming AI responses, and interactive UI
  
- How to manage concurrency when messages arrive faster than you can process
  
- How to choose the right deployment pattern for production
  

## What is Chat SDK?

Building a bot that works across multiple chat platforms usually means maintaining separate codebases, learning different APIs, and handling platform-specific quirks individually. Chat SDK hides those differences behind a unified interface, type-safe adapters, and an event-driven architecture.

A single handler written against Chat SDK fires for mentions on any connected platform:

```typescript
bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post("Hello! I'm listening to this thread.");
});
```

The same code runs whether the mention came from Slack, Teams, Discord, or Linear. The adapter for each platform handles webhook verification, message parsing, and API calls.

## How it works

Chat SDK has three core concepts:

- **Chat** is the main entry point. It coordinates adapters and routes events to your handlers.
  
- **Adapters** are platform-specific implementations. Each one handles webhook parsing, message formatting, and API calls for a single platform.
  
- **State** is a pluggable persistence layer for thread subscriptions and distributed locking.
  

When a webhook arrives, the Chat class identifies which adapter should handle it, parses the event into a platform-neutral shape, and dispatches it to your registered handlers. Any response you post goes back through the adapter, which converts your message into the platform's native format.

```typescript
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter(),
  },
  state: createRedisState(),
});

bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post("Hello! I'm listening to this thread.");
});
```

Each adapter factory auto-detects credentials from environment variables such as `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, and `REDIS_URL`, so you can run a bot with zero explicit config in most setups.

## Platform adapters

Adapters published under `@chat-adapter/*` and maintained by Vercel cover the most common platforms:

| Platform                      | Package                 | Notable capabilities                                                            |
| ----------------------------- | ----------------------- | ------------------------------------------------------------------------------- |
| Slack                         | `@chat-adapter/slack`   | Threads, reactions, interactive cards, modals, native streaming, Assistants API |
| Microsoft Teams               | `@chat-adapter/teams`   | Adaptive cards, mentions, conversation threading                                |
| Google Chat                   | `@chat-adapter/gchat`   | Spaces, threads, Workspace Events via Pub/Sub                                   |
| Discord                       | `@chat-adapter/discord` | Slash commands, threads, rich embeds                                            |
| GitHub                        | `@chat-adapter/github`  | Pull request and issue comment threads                                          |
| Linear                        | `@chat-adapter/linear`  | Issue comment threads and app-actor agent sessions                              |
| Telegram, Messenger, WhatsApp | `@chat-adapter/*`       | Channel-scoped conversations, groups, inline keyboards                          |

Community adapters extend this list to [Liveblocks](https://chat-sdk.dev/adapters/vendor-official/liveblocks), [Resend](https://chat-sdk.dev/adapters/vendor-official/resend), [Zernio](https://chat-sdk.dev/adapters/vendor-official/zernio), and others. Browse the full [adapter directory](https://chat-sdk.dev/adapters) or [build your own](https://chat-sdk.dev/docs/contributing/building).

Each adapter page documents its authentication options, supported features, and known limitations. For example, the Linear adapter distinguishes between `mode: "comments"` for issue comment webhooks and `mode: "agent-sessions"` for Linear app-actor installs.

To target more than one platform, register multiple adapters in the same [Chat instance](https://chat-sdk.dev/docs/usage):

```typescript
const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter(),
    teams: createTeamsAdapter(),
    gchat: createGoogleChatAdapter(),
  },
  state: createRedisState(),
});
```

The same `onNewMention` handler now fires for mentions on all three.

## Posting messages

`thread.post()` accepts several message formats. Pick the one that matches the content you're sending:

- **Plain string** for short replies. The text goes through as-is.
  
- **Markdown** with `{ markdown: "**Bold** text" }`. The SDK parses the markdown into an mdast AST, then each adapter converts it to the platform's format (mrkdwn for Slack, HTML for Teams, and so on).
  
- **AST** built with exported helpers like `root`, `paragraph`, `text`, and `link`. This gives you programmatic control without the overhead of cards.
  
- **Cards** for interactive UI with buttons, dropdowns, and structured layouts.
  
- **Streams** for real-time AI responses.
  

```typescript
// Plain text
await thread.post("Hello!");

// Markdown
await thread.post({ markdown: "**Deployment complete**" });

// AST
import { root, paragraph, text, strong, link } from "chat";
await thread.post({
  ast: root([
    paragraph([
      strong([text("Deployment complete")]),
      text(" — "),
      link("https://example.com", [text("View site")]),
    ]),
  ]),
});
```

For most cases, the AST builders give the best balance of control and simplicity.

## Streaming AI responses

Chat SDK accepts any `AsyncIterable<string>` as a message. You can pass an [AI SDK stream](https://ai-sdk.dev/docs/foundations/streaming) directly to [`thread.post()`](http://thread.post):

```typescript
import { ToolLoopAgent } from "ai";

const agent = new ToolLoopAgent({
  model,
  instructions: "You are a helpful assistant.",
});

const result = await agent.stream({ prompt: message.text });
await thread.post(result.fullStream);
```

The SDK uses platform-native streaming where available (e.g., Slack and Teams) and falls back to post-then-edit on other platforms. Use `fullStream` with multi-step agents because it preserves paragraph breaks between steps. `textStream` concatenates text across tool calls, which can produce run-on output.

For multi-turn conversations, use `toAiMessages()` to convert thread history into the `{ role, content }[]` format that AI SDKs expect:

```typescript
import { toAiMessages } from "chat";

bot.onSubscribedMessage(async (thread, message) => {
  const result = await thread.adapter.fetchMessages(thread.id, { limit: 20 });
  const history = await toAiMessages(result.messages);
  const response = await agent.stream({ prompt: history });
  await thread.post(response.fullStream);
});
```

The SDK also buffers potential GFM tables during streaming so they don't flash as raw pipe-delimited text before the structure is complete.

## Interactive UI with cards, actions, and modals

For buttons, dropdowns, and structured layouts, use cards. Register handlers with `onAction` to respond when a user clicks a button:

```typescript
bot.onAction("approve", async (event) => {
  await event.thread.post(`Order approved by ${event.user.fullName}!`);
});
```

Modals open form dialogs in response to button clicks or slash commands. They support text inputs, dropdowns, radio buttons, and server-side validation. Modals are currently supported on Slack and Teams:

```typescript
import { Modal, TextInput, Select, SelectOption } from "chat";

bot.onAction("feedback", async (event) => {
  await event.openModal(
    <Modal callbackId="feedback_form" title="Send Feedback" submitLabel="Send">
      <TextInput id="message" label="Your Feedback" multiline />
      <Select id="category" label="Category">
        <SelectOption label="Bug" value="bug" />
        <SelectOption label="Feature" value="feature" />
      </Select>
    </Modal>
  );
});
```

JSX card syntax requires `jsxImportSource: "chat"` in your `tsconfig.json`. If the types don't resolve, use the function-call syntax (`Modal({...})`) instead, which produces the same output.

## Handling overlapping messages

In production, a single user can send messages faster than your handler runs, especially on platforms like WhatsApp and Telegram where short, rapid messages are normal. When multiple messages arrive on the same thread while a handler is still processing, the SDK needs a strategy. Chat SDK offers four:

| Strategy         | Behavior                                                                                                                                   | When to use                                                               |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
| `drop` (default) | Discard new messages while a handler is running and throw a `LockError`                                                                    | Bots where losing rapid-fire duplicates is acceptable                     |
| `queue`          | Enqueue incoming messages, then process only the latest when the current handler finishes; pass intermediate messages as `context.skipped` | You want to acknowledge every message but respond once                    |
| `debounce`       | Wait for a pause in the conversation, then process only the final message                                                                  | WhatsApp, Telegram, or any chat where users send bursts of short messages |
| `concurrent`     | No locking. Every message runs in its own handler invocation                                                                               | Stateless handlers where thread ordering doesn't matter                   |

Configure the strategy on the `Chat` instance:

```typescript
const bot = new Chat({
  concurrency: "queue",
  lockScope: ({ isDM, adapter }) =>
    isDM ? "channel" : "thread",
  // ...
});

bot.onNewMention(async (thread, message, context) => {
  if (context && context.skipped.length > 0) {
    await thread.post(
      `You sent ${context.totalSinceLastHandler} messages while I was working. Responding to your latest.`
    );
  }
  const response = await generateAIResponse(message.text);
  await thread.post(response);
});
```

By default, locks are scoped to the thread. WhatsApp and Telegram adapters default to `lockScope: "channel"` because conversations happen at the channel level rather than in threads.

## State, subscriptions, and persistence

The state adapter handles two things: thread subscriptions (so `onSubscribedMessage` keeps firing after the initial mention) and distributed locking (so two serverless instances don't process the same webhook twice).

Available state adapters include:

- `@chat-adapter/state-memory` for local development and tests
  
- `@chat-adapter/state-redis` for production on standard Redis
  
- `@chat-adapter/state-ioredis` for Redis Cluster or Sentinel deployments
  
- A PostgreSQL state adapter for teams already running Postgres
  

Each thread also exposes typed, per-thread state with a 30-day TTL, which is useful for per-conversation preferences or in-flight workflow context:

```typescript
// Read state
const state = await thread.state;

// Merge into existing state
await thread.setState({ aiMode: true });

// Replace state entirely
await thread.setState({ aiMode: false }, { replace: true });
```

For local development, the memory state adapter works fine. For anything deployed to serverless infrastructure, use Redis or Postgres so subscriptions survive cold starts and multiple instances don't race each other.

## When to use Chat SDK

Chat SDK is the right choice when:

- You need the same bot behavior across multiple chat platforms
  
- You want type-safe event handlers instead of hand-rolled webhook parsers
  
- You're integrating AI responses and want streaming to work on every platform
  
- You're deploying to serverless and need distributed locking and message deduplication
  

It's probably the wrong choice when:

- You only target one platform and are already comfortable with its SDK
  
- Your bot is purely transactional with no threading, state, or streaming needs
  
- You need a feature that isn't yet supported by the adapter for your target platform (check the adapter's page for partial-support indicators)
  

## Deploying to production

A typical production deployment pairs Chat SDK with a serverless framework and a Redis-backed state adapter:

1. Create a webhook route per platform. Each adapter exposes a handler via `bot.webhooks.slack`, `bot.webhooks.teams`, and so on, which you wire into your framework's routing (Next.js route handlers, Hono, Nuxt server routes, etc.).
   
2. Provision Redis for state. Any Redis-compatible store works. Set `REDIS_URL` and the Redis state adapter auto-detects it.
   
3. Configure platform credentials as environment variables. Adapter factories pick them up automatically.
   
4. Register your webhook URLs with each platform (for example, the request URL in your Slack app manifest, the messaging endpoint in Azure Bot Service for Teams).
   
5. Handle concurrency explicitly. The default `drop` strategy is fine for low-traffic bots, but use `queue` or `debounce` if your users send messages in bursts.
   

### Build with Chat SDK today using these templates.

[**Chat SDK Form Bot**Build a bot that helps you address form submissions right from Slack, Teams, and more.](https://vercel.com/kb/guide/triage-form-submissions-with-chat-sdk)[**Chat SDK Deploy Bot**Build a bot that lets you deploy projects from Slack, with a built-in approval flow.](https://vercel.com/kb/guide/run-and-track-deploys-from-slack)

## FAQ

### Is Chat SDK free?

Yes, Chat SDK is free and open source. You can install it from npm, use it in commercial projects, and contribute to its development without any licensing cost. The official platform adapters published under `@chat-adapter/*` are also free to use. You will still need to pay for any third-party services your bot relies on, such as your chat platform's API tier, your AI model provider, and your Redis or Postgres host.

### Can I use any number of adapters?

Yes, you can register as many adapters as you need on a single [Chat](https://chat-sdk.dev/docs/usage) instance. Add each adapter to the `adapters` object when you create your bot, and the same handlers will run for events from every connected platform. This is the main reason to use Chat SDK: you write your bot logic once and it works across Slack, Teams, Discord, Linear, and any other platform you add later, without changing the rest of your code.

### What frameworks does Chat SDK support?

Chat SDK supports nearly any JavaScript or TypeScript framework, so you can add it to your existing codebase without changing your stack. It works with [Next.js](https://vercel.com/kb/guide/how-to-build-a-slack-bot-with-next-js-and-redis), [Nuxt](https://vercel.com/kb/guide/create-a-discord-support-bot-with-nuxt-and-redis), [Hono](https://vercel.com/kb/guide/ship-a-github-code-review-bot-with-hono-and-redis), SvelteKit, Express, and other server frameworks that can handle HTTP requests. Each platform adapter exposes a webhook handler (such as `bot.webhooks.slack`) that you wire into your framework's routing, whether that is a Next.js route handler, a Hono route, a Nuxt server route, or something else.

### Does Chat SDK only work on Vercel?

No, Chat SDK works anywhere you can run a Node.js or TypeScript server. It is framework-agnostic and runs on Next.js, Nuxt, Hono, and other frameworks, so you can deploy it to Vercel, AWS, Netlify, Cloudflare Workers, your own infrastructure, or any other host. Vercel maintains the SDK and the [official adapters](https://chat-sdk.dev/adapters), but there is no requirement to deploy on Vercel to use it.

## More Chat SDK guides

- [Build AI agents with AI Gateway and AI SDK](/kb/guide/ai-gateway-and-ai-sdk): Build AI agents on Vercel with AI Gateway and AI SDK, then make them reliable, capable, and durable with Sandbox, Chat SDK, Vercel Connect, and Workflow.
- [Using Chat SDK and eve together](/kb/guide/chat-sdk-and-eve): Understand how eve and Chat SDK divide responsibilities, when to use eve's first-class channels versus the Chat SDK channel, and how to bridge any Chat SDK adapter into an eve agent
- [Build Claude Managed Agents with Chat SDK](/kb/guide/claude-managed-agents-chat-sdk): Ship a Slack research bot built on Claude Managed Agents and Chat SDK. One persistent session per thread, streamed briefs with sources, and no Slack credentials to manage.