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>
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
- Install dependencies from the monorepo root:
pnpm install
- Copy the example environment file and fill in your platform credentials:
cp .env.example .env.local
- 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 AIto 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
- Add the four required
INSTAGRAM_*variables to.env.local. - Expose the app through an HTTPS tunnel or deploy it to Vercel.
- Configure Meta's callback URL as
https://your-domain.com/api/webhooks/instagram. - 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:
- Deploy a preview branch to Vercel
- Go to
/settingson the production deployment - Enter the preview branch URL and save
All webhook requests are proxied until the URL is cleared.