Skip to content
Docs

Chat SDK with TanStack Start

Use a TanStack Start React server route to receive Slack events and pass them to Chat SDK. The route runs on the server alongside your application's UI.

Start with an existing TanStack Start 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 @vercel/functions

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 src/routes/api/webhooks/slack.ts with a POST handler:

src/routes/api/webhooks/slack.ts
import { createFileRoute } from '@tanstack/react-router';
import { waitUntil } from '@vercel/functions';
import { bot } from '../../../../lib/bot';
 
export const Route = createFileRoute('/api/webhooks/slack')({
  server: {
    handlers: {
      POST: ({ request }) => bot.webhooks.slack(request, { waitUntil }),
    },
  },
});

The handler receives a Web Request and returns the adapter's Response. Pass waitUntil from @vercel/functions so the bot can finish processing after acknowledging the event. Import the bot only from server code.

Run your framework's development or build command to update the generated route tree. Keep the Nitro Vite plugin configured for Edgenivo so the server route is deployed as a function.

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 @tanstack/ai-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: 'ag-ui',
      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);
});

This example uses TanStack AI in the browser, so the Web Adapter must use protocol: 'ag-ui'. If you instead use the AI SDK React helper, select ai-sdk and install its client dependencies. See Web Adapter client options.

Create src/routes/api/chat.ts to receive browser requests and return the adapter's stream:

src/routes/api/chat.ts
import { createFileRoute } from '@tanstack/react-router';
import { waitUntil } from '@vercel/functions';
import { bot } from '../../../lib/bot';
 
export const Route = createFileRoute('/api/chat')({
  server: {
    handlers: {
      POST: ({ request }) => bot.webhooks.web(request, { waitUntil }),
    },
  },
});

Create src/routes/chat.tsx. Connect TanStack AI to the server route with fetchServerSentEvents:

src/routes/chat.tsx
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react';
import { createFileRoute } from '@tanstack/react-router';
 
export const Route = createFileRoute('/chat')({ component: ChatPage });
 
function ChatPage() {
  const { messages, sendMessage, status, error } = useChat({
    connection: fetchServerSentEvents('/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.content : ''))
            .join('')}
        </p>
      ))}
      <button disabled={busy} onClick={() => sendMessage('Hello')}>
        Send a test message
      </button>
      {error && (
        <p role="alert">Could not send your message. Please try again.</p>
      )}
    </section>
  );
}

TanStack AI sends a string with sendMessage(text) and exposes text parts as part.content. Keep the bot imported only in the server endpoint, and let the framework regenerate the route tree after adding both routes.

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 TanStack Start example.


Was this helpful?

supported.