Discord support bot with Nuxt and Redis
This guide walks through building a Discord support bot with Nuxt, covering project setup, Discord app configuration, Gateway forwarding, AI-powered responses, and deployment.
Prerequisites
- Node.js 18+
- pnpm (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:
npx nuxi@latest init my-discord-bot
cd my-discord-bot
pnpm add chat @chat-adapter/discord @chat-adapter/state-redis ai @ai-sdk/anthropicCreate a Discord app
- Go to discord.com/developers/applications
- Click New Application, give it a name, and click Create
- Go to Bot in the sidebar and click Reset Token — copy the token, you'll need this as
DISCORD_BOT_TOKEN - Under Privileged Gateway Intents, enable Message Content Intent
- Go to General Information and copy the Application ID and Public Key — you'll need these as
DISCORD_APPLICATION_IDandDISCORD_PUBLIC_KEY
Set up the Interactions endpoint
- In General Information, set the Interactions Endpoint URL to
https://your-domain.com/api/webhooks/discord - 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
- Go to OAuth2 in the sidebar
- Under OAuth2 URL Generator, select the
botscope - Under Bot Permissions, select:
- Send Messages
- Create Public Threads
- Send Messages in Threads
- Read Message History
- Add Reactions
- Use Slash Commands
- 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:
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_keyCreate 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:
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.`
);
});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".
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:
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:
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
- Start your development server (
pnpm dev) - Trigger the Gateway listener by visiting
http://localhost:3000/api/discord/gatewayin your browser - Expose your server with a tunnel (e.g.
ngrok http 3000) - Update the Interactions Endpoint URL in your Discord app settings to your tunnel URL (e.g.
https://abc123.ngrok.io/api/webhooks/discord) - @mention the bot in your Discord server — it should respond with a support card
- Reply in the thread — AI SDK should generate a response
- 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:
{
"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:
vercel deployAfter 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 — Build rich interactive messages with buttons, fields, and selects
- Actions — Handle button clicks, select menus, and other interactions
- Streaming — Stream AI-generated responses to chat
- Discord adapter — Full configuration reference and Gateway setup