Dashboard example from hbd.mjanglin.com
This page is a comprehensive guide on how to host a Discord bot on Vercel with Edge Functions. I'll cover everything from setting up a new Discord bot to deploying it to Vercel.
All of this wouldn't be possible without the work from @jzxhuang and his nextjs-discord-bot template:
This template provides you with the basic starter boiler plate for registering commands, and handling interactions with Discord on Edge runtime.
To extend that template further for the purpose of authentication and an interactive dashboard, I created a fork of it:
This template extends the base template to include:
- OAuth2 authentication with Discord
- Including encyption, cookie management, and JWT verification
- Prisma for database management (storing authenticated users)
- A simple dashboard to view authenticated users
For an extended overview on the changes made to the initial template and how to set it up, check out the pull request I made to the original repository.
git clone https://github.com/clxrityy/nextjs-discord-bot-with-oauth.git
.env
/.env.local
file:
# Settings -> General Information
DISCORD_APP_ID=
DISCORD_APP_PUBLIC_KEY=
# Settings -> OAuth2
DISCORD_CLIENT_SECRET=
# Settings -> Bot
# Required to register commands and fetch the list of commands in the web app
# Technically not required to actually run the bot
DISCORD_BOT_TOKEN=
pnpm install
pnpm dev
•/
├── prisma/ <-- Prisma schemas
├── public/ <-- Public assets
├── scripts/ <-- External scripts
├── src/ <-- Source code
├── ...
src/
├── app/ <-- Routes within the website
├── components/ <-- Components within website
├── data/ <-- Bot data
│ ├── commands/ <-- Bot commands
│ ├── util/ <-- Bot utilities
├── handlers/command/ <-- Command handlers
├── lib/ <-- External libraries
├── types/ <-- Types throughout the application
├── utils/ <-- Utility functions
├── config.ts <-- The configuration values
├── env.mjs <-- Environment variables
├── ...
Commands are defined as an ApplicationCommand
type.
export type ApplicationCommandOption = {
type: number;
name: string;
// ...
}
export type ApplicationCommand = {
name: string;
description: string;
options?: ApplicationCommandOption[];
permissions?: number[];
}
import { ApplicationCommand } from "@/types/interactions";
const PING_COMMAND: ApplicationCommand = {
name: "ping",
description: "Replies with Pong!"
} as const;
export default PING_COMMAND;
Every command is exported in the primary index.ts
file:
/**
* @see https://discord.com/developers/docs/interactions/application-commands#registering-a-command
*/
import PING_COMMAND from "./misc/ping";
export const commands = {
ping: PING_COMMAND,
}
pnpm register-commands
script.import { commands } from "@/data/commands";
import { env } from "./env.mjs";
/**
* @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
*/
async function main() {
const reponse = await fetch(`http://discord.com/api/v10/applications/${env.CLIENT_ID}/commands`,
{
headers: {
"Content-Type": "application/json",
"Authorization": `Bot ${env.BOT_TOKEN}`
},
method: "PUT",
body: JSON.stringify(Object.values(commands))
});
}
/api/interactions
).
tweetnacl
Within the interactions endpoint, data from the interaction is parsed into an InteractionData
interface:
export interface InteractionData {
id: string;
name: string;
options?: InteractionSubcommand<InteractionOption>[] | InteractionOption[] | InteractionSubcommandGroup<InteractionSubcommand<InteractionOption>>[];
}
This includes the value(s) of the option(s) and subcommands.
// api/interactions/route.ts
/**
* @see https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes
*/
export const runtime = "edge";
export async function POST(req: Reqest) {
/**
* @see https://github.com/clxrityy/mjanglin.com/blob/hbd/src/utils/verify.ts
*/
const verifyResult = await verifyInteractionRequest(req, env.PUBLIC_KEY);
const { interaction } = verifyResult;
}
The interaction is parsed into the
InteractionData
interface mentioned before
import { InteractionSubcommand, InteractionOption } from "@/types/interactions";
// ...
switch (name) {
case commands.ping.name:
return {
type: InteractionResponseType.ChannelMessageWithSource,
data: {
content: "Pong!"
}
}
}
Prisma is a modern database toolkit that makes it easy to work with databases. It provides a type-safe and auto-generated query builder that's tailored to your database schema.
pnpm add @prisma/client @prisma/extension-accelerate
pnpm add -D prisma
npx prisma generate
.
// src/lib/db.ts
import { PrismaClient } from "@prisma/client";
import { withAccelerate } from "@prisma/extension-accelerate";
function makePrisma() {
return new PrismaClient({
datasources: {
db: {
url: process.env.ACCELERATE_URL
}
}
}).$extends(withAccelerate());
}
prisma/
directory.User
model:
model User {
id String @id @default(uuid())
userId String @unique
accessToken String @unique
refreshToken String @unique
// More added later
}
npx prisma db push
to create the table in your database, and npx prisma generate
to generate the Prisma client.
User
model in your application.import { db } from "@/lib/db";
export async function getUser(userId: string) {
return await db.user.findFirst({
where: {
userId: userId
}
});
}
Thank you for reading! ❤️