---
title: Discord support bot with Nuxt and Redis
description: This guide walks through building a Discord support bot with Nuxt, covering project setup, Discord app configuration, Gateway forwarding, AI-powered responses, and deployment.
type: guide
prerequisites: []
related:
  - /adapters/discord
  - /docs/cards
  - /docs/actions
---

## Prerequisites

- Node.js 18+
- [pnpm](https://pnpm.io) (or npm/yarn)
- A Discord server where you have admin access
- A Redis instance for state management

## Create a Nuxt app

Scaffold a new Nuxt project and install Chat SDK dependencies:

```sh title="Terminal"
npx nuxi@latest init my-discord-bot
cd my-discord-bot
pnpm add chat @chat-adapter/discord @chat-adapter/state-redis ai @ai-sdk/anthropic
```

## Create a Discord app

1. Go to [discord.com/developers/applications](https://discord.com/developers/applications)
2. Click **New Application**, give it a name, and click **Create**
3. Go to **Bot** in the sidebar and click **Reset Token** — copy the token, you'll need this as `DISCORD_BOT_TOKEN`
4. Under **Privileged Gateway Intents**, enable **Message Content Intent**
5. Go to **General Information** and copy the **Application ID** and **Public Key** — you'll need these as `DISCORD_APPLICATION_ID` and `DISCORD_PUBLIC_KEY`

### Set up the Interactions endpoint

1. In **General Information**, set the **Interactions Endpoint URL** to `https://your-domain.com/api/webhooks/discord`
2. Discord will send a PING to verify the endpoint — you'll need to deploy first or use a tunnel

### Invite the bot to your server

1. Go to **OAuth2** in the sidebar
2. Under **OAuth2 URL Generator**, select the `bot` scope
3. Under **Bot Permissions**, select:
   - Send Messages
   - Create Public Threads
   - Send Messages in Threads
   - Read Message History
   - Add Reactions
   - Use Slash Commands
4. Copy the generated URL and open it in your browser to invite the bot

## Configure environment variables

Create a `.env` file in your project root:

```bash title=".env"
DISCORD_BOT_TOKEN=your_bot_token
DISCORD_PUBLIC_KEY=your_public_key
DISCORD_APPLICATION_ID=your_application_id
REDIS_URL=redis://localhost:6379
ANTHROPIC_API_KEY=your_anthropic_api_key
```

## Create the bot

Create `server/lib/bot.ts` with a `Chat` instance configured with the Discord adapter. This bot uses AI SDK to answer support questions:

```typescript title="server/lib/bot.tsx" lineNumbers
import { Chat, Card, CardText as Text, Actions, Button, Divider } from "chat";
import { createDiscordAdapter } from "@chat-adapter/discord";
import { createRedisState } from "@chat-adapter/state-redis";
import { generateText } from "ai";
import { anthropic } from "@ai-sdk/anthropic";

export const bot = new Chat({
  userName: "support-bot",
  adapters: {
    discord: createDiscordAdapter(),
  },
  state: createRedisState(),
});

bot.onNewMention(async (thread) => {
  await thread.subscribe();
  await thread.post(
    <Card title="Support">
      <Text>Hey! I'm here to help. Ask your question in this thread and I'll do my best to answer it.</Text>
      <Divider />
      <Actions>
        <Button id="escalate" style="danger">Escalate to Human</Button>
      </Actions>
    </Card>
  );
});

bot.onSubscribedMessage(async (thread, message) => {
  await thread.startTyping();

  const { text } = await generateText({
    model: anthropic("claude-sonnet-4-5-20250514"),
    system: "You are a friendly support bot. Answer questions concisely. If you don't know the answer, say so and suggest the user click 'Escalate to Human'.",
    prompt: message.text,
  });

  await thread.post(text);
});

bot.onAction("escalate", async (event) => {
  await event.thread.post(
    `${event.user.fullName} requested human support. A team member will follow up shortly.`
  );
});
```

<Callout type="info">
  The file extension must be `.tsx` (not `.ts`) when using JSX components like `Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"` and `"jsxImportSource": "chat"`.
</Callout>

`onNewMention` fires when a user @mentions the bot. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage` where AI SDK generates a response.

## Create the webhook route

Create a server route that handles incoming Discord webhooks:

```typescript title="server/api/webhooks/[platform].post.ts" lineNumbers
import { bot } from "../lib/bot";

type Platform = keyof typeof bot.webhooks;

export default defineEventHandler(async (event) => {
  const platform = getRouterParam(event, "platform") as Platform;

  const handler = bot.webhooks[platform];
  if (!handler) {
    throw createError({ statusCode: 404, message: `Unknown platform: ${platform}` });
  }

  const request = toWebRequest(event);

  return handler(request, {
    waitUntil: (task) => event.waitUntil(task),
  });
});
```

This creates a `POST /api/webhooks/discord` endpoint. The `waitUntil` option ensures message processing completes after the HTTP response is sent.

## Set up the Gateway forwarder

Discord doesn't push messages to webhooks like Slack does. Instead, messages arrive through the Gateway WebSocket. The Discord adapter includes a built-in Gateway listener that connects to the WebSocket and forwards events to your webhook endpoint.

Create a route that starts the Gateway listener:

```typescript title="server/api/discord/gateway.get.ts" lineNumbers
import { bot } from "../../lib/bot";

export default defineEventHandler(async (event) => {
  await bot.initialize();

  const discord = bot.getAdapter("discord");
  if (!discord) {
    throw createError({ statusCode: 404, message: "Discord adapter not configured" });
  }

  const baseUrl = process.env.NUXT_PUBLIC_SITE_URL || "http://localhost:3000";
  const webhookUrl = `${baseUrl}/api/webhooks/discord`;

  const durationMs = 10 * 60 * 1000; // 10 minutes

  return discord.startGatewayListener(
    { waitUntil: (task: Promise<unknown>) => event.waitUntil(task) },
    durationMs,
    undefined,
    webhookUrl,
  );
});
```

The Gateway listener connects to Discord's WebSocket, receives messages, and forwards them to your webhook endpoint for processing. In production, you'll want a cron job to restart it periodically.

## Test locally

1. Start your development server (`pnpm dev`)
2. Trigger the Gateway listener by visiting `http://localhost:3000/api/discord/gateway` in your browser
3. Expose your server with a tunnel (e.g. `ngrok http 3000`)
4. Update the **Interactions Endpoint URL** in your Discord app settings to your tunnel URL (e.g. `https://abc123.ngrok.io/api/webhooks/discord`)
5. @mention the bot in your Discord server — it should respond with a support card
6. Reply in the thread — AI SDK should generate a response
7. Click **Escalate to Human** — the bot should post an escalation message

## Add a cron job for production

The Gateway listener runs for a fixed duration. In production, set up a cron job to restart it automatically. If you're deploying to Vercel, add a `vercel.json`:

```json title="vercel.json"
{
  "crons": [
    {
      "path": "/api/discord/gateway",
      "schedule": "*/9 * * * *"
    }
  ]
}
```

This restarts the Gateway listener every 9 minutes, ensuring continuous connectivity. Protect the endpoint with a `CRON_SECRET` environment variable in production.

## Deploy to Vercel

Deploy your bot to Vercel:

```sh title="Terminal"
vercel deploy
```

After deployment, set your environment variables in the Vercel dashboard (`DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY`, `DISCORD_APPLICATION_ID`, `REDIS_URL`, `ANTHROPIC_API_KEY`). Update the **Interactions Endpoint URL** in your Discord app settings to your production URL.

## Next steps

- [Cards](/docs/cards) — Build rich interactive messages with buttons, fields, and selects
- [Actions](/docs/actions) — Handle button clicks, select menus, and other interactions
- [Streaming](/docs/streaming) — Stream AI-generated responses to chat
- [Discord adapter](/adapters/discord) — Full configuration reference and Gateway setup
- [State Adapters](/docs/state) — PostgreSQL, ioredis, and other state adapter options
