Files
vercel__chat/examples/nextjs-chat
OSS Polar Bear 169788b65a feat(chat): introduce unified History API with user, thread, and chan… (#592)
Adds `bot.history` as the canonical entry point for message history,
with three scopes: `user`, `thread`, and `channel`. `bot.transcripts`
stays as a deprecated alias, so nothing breaks.

## Why

History access was spread across `bot.transcripts`, `thread.messages` /
`thread.allMessages`, and per-adapter calls. `bot.history` puts the
promise-based read paths in one place, and the AI tools
(`fetchMessages`, `fetchChannelMessages`, `listThreads`) now route
through it.

## User scope

Cross-platform per-user persistence, identical in surface to
`bot.transcripts`:

```typescript
const bot = new Chat({
  adapters: { slack, telegram },
  state,
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});

await bot.history.user.append(thread, message);
const entries = await bot.history.user.list({ userKey, limit: 20 });
await bot.history.user.delete({ userKey });
```

The new `toPromptEntries` helper turns those entries into `{ role,
content }` messages for an LLM call:

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

const entries = await bot.history.user.list({ userKey });
const { text } = await generateText({
  model,
  messages: toPromptEntries(entries),
});
```

## Thread scope

Single-page reads and an auto-paginating generator:

```typescript
// One page, newest messages by default
const { messages, nextCursor } = await bot.history.thread.list(thread.id, {
  limit: 20,
});

// Everything, oldest first, pagination handled for you
for await (const msg of bot.history.thread.collect(thread.id, { limit: 50 })) {
  console.log(msg.text);
}
```

## Channel scope

```typescript
// Top-level channel messages (not thread replies)
const { messages } = await bot.history.channel.listMessages("slack:C123", {
  limit: 20,
});

// Thread listings
const { threads } = await bot.history.channel.listThreads("slack:C123");

// Threads together with a page of messages each
const result = await bot.history.channel.listThreadsWithMessages("slack:C123", {
  maxThreads: 5,
  messagesPerThread: 10,
});
```

## Semantics

The read paths are strict about where data comes from:

- The adapter named in the ID prefix must be registered. A typo'd or
unknown prefix throws instead of reading as an empty conversation.
- The SDK-side `ThreadHistoryCache` only serves adapters that persist
history there (`persistThreadHistory: true`, e.g. Telegram, WhatsApp).
For every other adapter the platform response is authoritative, so an
empty page is a real empty page, and a `cursor` always returns the
adapter's response as-is.
- Cache reads honor the same windows as adapter reads: backward
(default) gives the newest N, forward the oldest N, and `collect()`
yields the oldest N on both paths.
- `channel.listMessages` throws a capability error on adapters without
`fetchChannelMessages` (persisting adapters are served from the
channel-keyed cache instead), and `listThreadsWithMessages` fetches
per-thread pages through `history.thread.list` a few threads at a time
to stay inside platform rate limits.

## Migration

```typescript
// Before
const bot = new Chat({
  identity: ({ author }) => author.email ?? null,
  transcripts: { retention: "30d", maxPerUser: 200 },
});
await bot.transcripts.append(thread, msg);

// After
const bot = new Chat({
  history: {
    user: {
      identity: ({ author }) => author.email ?? null,
      retention: "30d",
      maxPerUser: 200,
    },
  },
});
await bot.history.user.append(thread, msg);
```

You can migrate one field at a time: when both `history.user` and the
legacy `transcripts` block are set they merge, with `history.user`
winning field by field, so settings left on `transcripts` keep applying
until you move them. `TranscriptEntry` is deprecated in favour of
`HistoryEntry` (also exported as `UserHistoryEntry`); all deprecated
names keep working in the current major version.

## Included

- New `packages/chat/src/history/` module with unit tests for every
scope
- AI tools rewired to `bot.history`, keeping their scope guards
- The nextjs example uses the new APIs throughout, with Thread History
and Channel History test buttons that exercise every scope
- Docs: `/docs/history` guide, `/docs/api/history` reference,
deprecation callouts on the transcripts pages
- Changeset (`minor` for `chat`)

---------

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-08-28 18:47:26 +10:00
..

Next.js Chat Example

A full-featured example app demonstrating the Chat SDK with Next.js. Integrates with Slack, Microsoft Teams, Google Chat, Discord, GitHub, Linear, Instagram, and other supported platforms — configure whichever platforms you need via environment variables.

Getting started

Prerequisites

  • Node.js 20+
  • pnpm 9+
  • Redis (for state persistence)
  • At least one platform configured (see Environment variables)

Setup

  1. Install dependencies from the monorepo root:
pnpm install
  1. Copy the example environment file and fill in your platform credentials:
cp .env.example .env.local
  1. Start the dev server:
pnpm dev

The app runs at http://localhost:3000. Platform webhooks should point to /api/webhooks/{platform} (e.g. /api/webhooks/slack).

For local development with real webhooks, use a tunneling tool like ngrok or localtunnel.

What it demonstrates

  • Event handlers — mentions, thread subscriptions, pattern matching, reactions
  • AI mode — @mention AI to enable streaming LLM responses via the Vercel AI SDK
  • Cards — interactive JSX-based cards with buttons, dropdowns, and fields
  • Modals — form dialogs with text inputs, validation, and private metadata
  • Actions — button clicks and dropdown selections with response handlers
  • Slash commands — platform-specific command handling
  • Ephemeral messages — user-only visible messages with DM fallback
  • DMs — programmatic direct message initiation
  • File uploads — attachment detection and display
  • Multi-platform — same bot logic across all six platforms

Project structure

src/
├── app/
│   ├── api/
│   │   ├── webhooks/[platform]/route.ts   # Main webhook entry point
│   │   └── discord/gateway/route.ts        # Discord gateway cron
│   ├── settings/page.tsx                   # Preview branch config UI
│   └── page.tsx                            # Home page
├── lib/
│   ├── bot.tsx                             # Bot logic and handlers
│   ├── adapters.ts                         # Adapter initialization
│   └── recorder.ts                         # Webhook recording system
└── middleware.ts                            # Preview branch proxy

Environment variables

Copy .env.example for the full list. At minimum, set BOT_USERNAME and credentials for one platform:

Variable Description
BOT_USERNAME Bot display name
SLACK_CONNECTOR Slack Vercel Connect connector UID (e.g. slack/acme-slack)
SLACK_AGENT_VIEW Set to true when your Slack manifest uses agent_view (see the commented blocks in slack-manifest.yml) — enables the Agent messaging experience with auto-applied suggested prompts
SLACK_NATIVE_STREAMING Set to false to stream via post-and-edit (chat.update) instead of Slack's native streaming API — useful on Slack flavours without chat.startStream (e.g. GovSlack)
VERCEL_OIDC_TOKEN Vercel OIDC token used by Connect (run vercel env pull)
TEAMS_APP_ID Teams app ID
TEAMS_APP_PASSWORD Teams app password
GOOGLE_CHAT_CREDENTIALS Google Chat service account JSON
DISCORD_BOT_TOKEN Discord bot token
DISCORD_PUBLIC_KEY Discord interaction verification key
INSTAGRAM_ACCESS_TOKEN Instagram User access token
INSTAGRAM_ACCOUNT_ID Instagram professional account ID returned by /me?fields=user_id,username
INSTAGRAM_APP_SECRET Meta app secret used to verify Instagram webhook signatures
INSTAGRAM_VERIFY_TOKEN Private value configured as the Instagram webhook verify token
GITHUB_CONNECTOR GitHub Vercel Connect connector UID (needs VERCEL_OIDC_TOKEN — run vercel env pull)
LINEAR_CONNECTOR Linear Vercel Connect connector UID (needs VERCEL_OIDC_TOKEN — run vercel env pull)
LINEAR_MODE Linear inbound mode: comments or agent-sessions
NOTION_TOKEN Notion connection access token
NOTION_VERIFICATION_TOKEN Notion webhook HMAC key (from subscription verification handshake)
NOTION_MENTION_MODE Optional: mention | all-comments | keyword (adapter default mention)
NOTION_KEYWORDS Optional: comma-separated keywords when NOTION_MENTION_MODE=keyword
REDIS_URL Redis connection string

See the Chat SDK docs for full platform setup guides.

For Linear app-actor mode, set LINEAR_MODE=agent-sessions, enable Agent session events on the webhook, install the Linear app with actor=app and app:mentionable, and keep using the existing thread.startTyping() / thread.post(...) handler flow. The adapter maps those calls onto Linear agent activities automatically.

Test Instagram DMs

  1. Add the four required INSTAGRAM_* variables to .env.local.
  2. Expose the app through an HTTPS tunnel or deploy it to Vercel.
  3. Configure Meta's callback URL as https://your-domain.com/api/webhooks/instagram.
  4. Subscribe the professional account to the webhook fields:
curl -X POST \
  "https://graph.instagram.com/v26.0/me/subscribed_apps?subscribed_fields=messages,message_reactions,messaging_postbacks,messaging_seen&access_token=$INSTAGRAM_ACCESS_TOKEN"

Send the professional account a DM from another Instagram account. Normal messages run the example's AI response; send post-card to test Instagram quick replies without invoking the model.

Recording and replay

The app includes a recording system for capturing production webhook interactions and converting them into replay tests.

# Enable recording in your environment
RECORDING_ENABLED=true

# List recorded sessions
pnpm recording:list

# Export a session
pnpm recording:export <session-id>

See packages/integration-tests/fixtures/replay/README.md for the full workflow.

Preview branch testing

Test PRs with real webhook traffic by proxying requests from production to a preview deployment:

  1. Deploy a preview branch to Vercel
  2. Go to /settings on the production deployment
  3. Enter the preview branch URL and save

All webhook requests are proxied until the URL is cleared.