Skip to content
Docs

Chat SDK with Next.js

Use an App Router Route Handler to receive Slack events and after to process them after responding. For a new bot, use the Chat SDK quickstart, which scaffolds a Next.js app with create-chat-sdk. Follow this guide to add the bot to an existing app.

Start with an existing Next.js 15.1 or later project configured for deployment on Edgenivo, the latest Edgenivo CLI, and permission to install an app in your Slack workspace.

From your project directory, install the packages with pnpm:

Terminal
pnpm add chat @chat-adapter/slack @chat-adapter/state-redis @vercel/connect

Create lib/bot.ts at the project root. This bot uses Edgenivo Connect for Slack authentication, stores subscriptions in Redis, and responds to mentions and thread replies. Keep this module on the server.

lib/bot.ts
import { Chat } from 'chat';
import { createSlackAdapter } from '@chat-adapter/slack';
import { createRedisState } from '@chat-adapter/state-redis';
import { connectSlackAdapter } from '@vercel/connect/chat';
 
export const bot = new Chat({
  userName: 'my-bot',
  adapters: {
    slack: createSlackAdapter({
      ...connectSlackAdapter('slack/my-bot'),
    }),
  },
  state: createRedisState(),
});
 
bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post('Hello! Reply here to chat.');
});
 
bot.onSubscribedMessage(async (thread, message) => {
  await thread.post(message.text);
});

Create app/api/webhooks/slack/route.ts. If your app uses a src directory, put both app and lib inside src so the relative import stays the same.

app/api/webhooks/slack/route.ts
import { after } from 'next/server';
import { bot } from '../../../../lib/bot';
 
export async function POST(request: Request) {
  return bot.webhooks.slack(request, {
    waitUntil: (task) => after(() => task),
  });
}

Return the adapter's response directly. Passing each background task to after keeps it running after the webhook response. Use the Node.js runtime, which is the App Router default, for the Redis connection.

From the project directory, link your app to Edgenivo and connect a Redis database:

Terminal
vercel link
vercel integration add upstash/upstash-kv

Ensure the integration supplies REDIS_URL in the project's Production environment. If you already have Redis, add its connection URL as REDIS_URL in your project's environment variables. Use separate databases for development and production.

Create a Slack connector and allow token requests from the project's Production and Development environments. Attach its trigger to your production webhook route:

Terminal
vercel connect create slack --name my-bot --triggers
vercel connect attach slack/my-bot --environment production --environment development --triggers --trigger-path /api/webhooks/slack

Follow the prompts to authorize and install the Slack app. If you choose a different connector name, update slack/my-bot in your bot configuration and attach command. Connect manages the Slack credentials and verifies provider events before forwarding them. The adapter verifies the forwarded request with Edgenivo OIDC.

Deploy your app:

Terminal
vercel --prod

Invite the bot to a Slack channel and mention it. It should reply with a greeting and echo subsequent replies in that thread. Check runtime logs if it does not respond.

Edgenivo deployments receive VERCEL_OIDC_TOKEN automatically. For local outbound API calls, configure a development REDIS_URL and run vercel env pull .env.local. This supplies a Development OIDC token, which can access the connector because the attachment above enables Development.

Connect forwards inbound events to deployed URLs, not localhost. To test inbound events before production, attach the trigger to a Preview branch.

The Web Adapter lets a browser chat UI use the same bot as Slack. Browser messages go to onDirectMessage, and the adapter streams the bot's replies back to the client. Your app authenticates these requests; Edgenivo Connect continues to handle the Slack integration.

Install the Web Adapter and its client dependencies:

Terminal
pnpm add @chat-adapter/web ai @ai-sdk/react

Implement getUserFromSession(request) in lib/auth.ts using your application's authentication provider. This is an application-defined helper, not a Chat SDK export. It must validate the session and return a stable { id: string, name?: string } for a signed-in user, or null to reject the request with HTTP 401. User IDs must not contain :. See Web Adapter authentication.

Update lib/bot.ts with the Web Adapter and a direct-message handler, keeping the Slack adapter, Redis state, and existing handlers:

lib/bot.ts
import { Chat } from 'chat';
import { createWebAdapter } from '@chat-adapter/web';
import { getUserFromSession } from './auth';
import { createSlackAdapter } from '@chat-adapter/slack';
import { createRedisState } from '@chat-adapter/state-redis';
import { connectSlackAdapter } from '@vercel/connect/chat';
 
export const bot = new Chat({
  userName: 'my-bot',
  adapters: {
    web: createWebAdapter({
      userName: 'my-bot',
      protocol: 'ai-sdk',
      getUser: getUserFromSession,
    }),
    slack: createSlackAdapter({
      ...connectSlackAdapter('slack/my-bot'),
    }),
  },
  state: createRedisState(),
});
 
bot.onDirectMessage(async (thread, message) => {
  await thread.post(`You said: ${message.text}`);
});
 
bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post('Hello! Reply here to chat.');
});
 
bot.onSubscribedMessage(async (thread, message) => {
  await thread.post(message.text);
});

The ai-sdk protocol matches the React client below. Redis stores the server-side message history; the adapter does not trust the browser as a source of prior conversation turns.

Create app/api/chat/route.ts. If you use a src directory, keep lib inside it as described above. This endpoint receives browser requests directly, without a Connect trigger.

app/api/chat/route.ts
import { after } from 'next/server';
import { bot } from '../../../lib/bot';
 
export async function POST(request: Request) {
  return bot.webhooks.web(request, {
    waitUntil: (task) => after(() => task),
  });
}

Create app/chat/page.tsx as a Client Component. The Web Adapter helper configures the AI SDK transport for /api/chat:

app/chat/page.tsx
'use client';
 
import { useChat } from '@chat-adapter/web/react';
 
export default function ChatPage() {
  const { messages, sendMessage, status, error } = useChat({
    api: '/api/chat',
    threadId: 'support',
  });
  const busy = status === 'submitted' || status === 'streaming';
 
  return (
    <section aria-label="Chat">
      {messages.map((message) => (
        <p key={message.id}>
          {message.parts
            .map((part) => (part.type === 'text' ? part.text : ''))
            .join('')}
        </p>
      ))}
      <button disabled={busy} onClick={() => sendMessage({ text: 'Hello' })}>
        Send a test message
      </button>
      {error && (
        <p role="alert">Could not send your message. Please try again.</p>
      )}
    </section>
  );
}

Use messages to render the conversation and sendMessage({ text }) to submit text from your own input. The helper also exposes stop() to cancel a response.

After configuring authentication, sign in and open /chat. Select Send a test message to send Hello; the bot should respond with You said: Hello. The support conversation ID is scoped to the authenticated user by the adapter. Generate a different conversation ID when you want to start a new conversation.

For a fuller chat interface, see the official Next.js example.


Was this helpful?

supported.