Getting Started
Build your first Discord bot with Calabasas and Convex
This tutorial walks you through creating a Discord bot from scratch. By the end, you'll have a working bot with a /ping command and a /server-info command that queries live Discord data synced to your Convex database.
What you'll build
/pingSimple responseA basic command that responds with "Pong!" — your hello world for Discord bots.
/server-infoSynced dataQueries guild, channel, and role data that Calabasas automatically syncs to your Convex database.
Prerequisites
A Convex project. If you don't have one, run npm create convex@latest
A Discord account with access to the Developer Portal
Node.js 18+ or Bun installed on your machine
A Discord server where you have permission to add bots
Create a Discord Application
Head to the Discord Developer Portal and create a new application.
a. Create the application
Click New Application, give it a name (e.g. "My Convex Bot"), and save. Copy the Application ID from the General Information page — you'll need it later.
b. Create the bot & get your token
Go to the Bot tab and click Reset Token to generate a bot token. Copy it immediately — you won't be able to see it again.
c. Enable Gateway Intents
Still on the Bot tab, scroll down to Privileged Gateway Intents and enable:
Keep your bot token secret. Never commit it to source control or share it publicly. Calabasas encrypts it before storage.
Login to Calabasas
Authenticate the CLI with your Calabasas account. This opens your browser to complete the OAuth flow.
npx calabasas loginYour API key is saved to ~/.calabasas/config.json.
Initialize your project
Run this from the root of your Convex project. It creates the configuration file that tells Calabasas what to sync and which events to handle.
npx calabasas initThis creates convex/calabasas.config.ts with a starter template.
Configure sync, events, and commands
Edit convex/calabasas.config.ts to define what your bot does. For this tutorial, we'll sync guild data, handle interactions, and register two slash commands.
import { defineCalabasas } from "calabasas";
export default defineCalabasas({
// Sync Discord data automatically to your Convex tables
sync: {
guilds: true, // Server info (name, member count, boost tier)
channels: true, // All channels (text, voice, categories)
roles: true, // All roles (name, color, permissions)
members: false, // Enable later if you need member data
},
// Events forwarded to your calabasas/discord:receive mutation
events: {
interactionCreate: true, // Slash commands, buttons, modals
},
// Slash commands registered with Discord
commands: {
ping: {
description: "Check if the bot is alive",
},
"server-info": {
description: "View server information",
},
},
});The sync config tells Calabasas to keep your Convex tables up-to-date with Discord data in real time. When a channel is created or a role is updated in Discord, your tables update automatically.
Register your Discord bot
This interactive wizard connects your Discord bot to Calabasas. Have your Application ID and bot token ready from Step 1.
npx calabasas bot addThe wizard will ask for:
Bot name — A friendly name for your reference
Discord Application ID — From the General Information page
Bot token — The token you copied earlier (encrypted before storage)
Convex URL — Your deployment URL (e.g. https://your-project.convex.cloud)
Gateway intents — Select GUILDS and GUILD_MESSAGES at minimum
After registration, the CLI outputs a shared secret. Save this — you'll need it as the CALABASAS_SECRET environment variable in your Convex deployment.
Generate type-safe handlers
This reads your config and generates TypeScript types, validators, and handler wrappers tailored to your setup.
npx calabasas generateThis creates several files:
convex/discord.generated.ts— Event handler wrapper and TypeScript typesconvex/calabasas/schema.ts— Sync table definitionsconvex/calabasas/sync.ts— Sync mutations for the gatewayAdd sync tables to your schema
Import the generated table definitions and spread them into your Convex schema. This creates tables like calabasasGuilds, calabasasChannels, and calabasasRoles in your database.
import { defineSchema } from "convex/server";
import { calabasasTables } from "./calabasas/schema";
export default defineSchema({
...calabasasTables,
// Add your own tables here as your bot grows:
// messages: defineTable({ ... }),
// warnings: defineTable({ ... }),
});The sync tables are indexed for efficient queries. For example, calabasasGuilds has a by_discord_id index, and calabasasChannels has a by_guild index for looking up all channels in a server.
Implement your command handlers
This is where the magic happens
Create convex/calabasas/discord.ts with your command handlers. The gateway calls this file's receive export whenever a Discord event arrives.
import { handleDiscordEvent, InteractionType } from "./_generated/discord";
import type { ApplicationCommandData } from "./_generated/discord";
export const receive = handleDiscordEvent({
interactionCreate: async (ctx, event) => {
// Only handle slash commands (not buttons, modals, etc.)
if (event.type !== InteractionType.ApplicationCommand) return;
const { name } = event.data as ApplicationCommandData;
// /ping - Simple response
if (name === "ping") {
return { content: "Pong!" };
}
// /server-info - Query synced Discord data from your Convex database
if (name === "server-info") {
const guildId = event.guildId;
if (!guildId) {
return {
content: "This command can only be used in a server.",
ephemeral: true,
};
}
// Look up the guild from the automatically synced table
const guild = await ctx.db
.query("calabasasGuilds")
.withIndex("by_discord_id", (q) => q.eq("discordId", guildId))
.unique();
if (!guild) {
return {
content: "Server data is still syncing. Try again in a moment.",
ephemeral: true,
};
}
// Count channels by type
const channels = await ctx.db
.query("calabasasChannels")
.withIndex("by_guild", (q) => q.eq("guildDiscordId", guildId))
.collect();
const textChannels = channels.filter((c) => c.type === 0).length;
const voiceChannels = channels.filter((c) => c.type === 2).length;
// Count roles
const roles = await ctx.db
.query("calabasasRoles")
.withIndex("by_guild", (q) => q.eq("guildDiscordId", guildId))
.collect();
const boostTier = ["None", "Tier 1", "Tier 2", "Tier 3"];
return {
content: [
`### ${guild.name}`,
`**Members:** ${guild.memberCount ?? "Unknown"}`,
`**Channels:** ${textChannels} text, ${voiceChannels} voice`,
`**Roles:** ${roles.length}`,
`**Boost Tier:** ${boostTier[guild.premiumTier] ?? "Unknown"}`,
`**Owner:** <@${guild.ownerId}>`,
].join("\n"),
};
}
},
});Key patterns to understand
handleDiscordEvent({ ... })Generated wrapper that validates the shared secret, routes events to your handlers, and returns the response to the gateway.
InteractionType.ApplicationCommandDiscord sends multiple interaction types through the same event. Check for ApplicationCommand (type 2) to handle slash commands specifically. Other types include MessageComponent (3), Autocomplete (4), and ModalSubmit (5).
event.data as ApplicationCommandDataAfter narrowing the interaction type, cast event.data to get type-safe access to the command name, options, and resolved data.
{ ephemeral: true }Ephemeral responses are only visible to the user who ran the command. Use them for error messages and private info.
ctx.db.query("calabasasGuilds").withIndex(...)Synced tables are regular Convex tables. Query them like any other table using .withIndex(). The data stays up-to-date automatically as Discord changes.
The sync mutations must be re-exported from convex/discord.ts so the gateway can call them as discord:syncGuild, discord:syncChannel, etc. Don't forget this line!
Set environment variables
Add the shared secret from Step 5 to your Convex deployment. This authenticates requests from the gateway.
# For local development
npx convex env set CALABASAS_SECRET your-shared-secret-here
# Or add to your Convex dashboard under Settings > Environment VariablesYou can also set CALABASAS_GATEWAY_URL if you plan to use Discord Actions (sending messages, banning users, etc. from Convex). This isn't needed for the tutorial.
Push your configuration
Upload your config to Calabasas. This tells the gateway to start syncing data and registers your slash commands with Discord.
npx calabasas pushThe CLI will show you a summary of your sync settings, events, and commands. If you have multiple bots, it will prompt you to select one.
Global slash commands can take up to 1 hour to appear in Discord. If you don't see them immediately, wait a bit and try again.
Invite the bot to your server
Go back to the Discord Developer Portal and generate an invite link.
- Go to your application → Installation
- Under Guild Install, add the
botandapplications.commandsscopes - Select the permissions your bot needs (at minimum: Send Messages and Use Slash Commands)
- Copy the install link and open it in your browser
- Select your server and authorize
Test your bot!
Make sure your Convex dev server is running (npx convex dev), then head to your Discord server and try:
/pingPong!
/server-info### My Server **Members:** 42 **Channels:** 5 text, 2 voice **Roles:** 8 **Boost Tier:** Tier 1 **Owner:** @you
If /server-info says "Server data is still syncing", wait a few seconds. The sync happens when the bot first connects to your server.
Next steps
Add command options
Commands can accept typed parameters like strings, numbers, users, channels, and roles. Add an options array to your command config and extract values with options?.find(o => o.name === "user")?.value.
Handle message events
Add messageCreate: true to your events config and implement a messageCreate handler to store messages, build activity feeds, or create auto-moderation.
Use Discord Actions
Send messages, ban users, manage roles, and more from your Convex actions using the generated discord helper. Set CALABASAS_GATEWAY_URL and use ctx.scheduler.runAfter() to trigger actions from command handlers.
Enable member sync
Set members: true in your sync config to sync member data (usernames, nicknames, roles, join dates). Requires the Server Members Intent in the Discord Developer Portal.
Monitor with the dashboard
Run npx calabasas status or npx calabasas logs to see real-time event logs and connection status in your terminal.
Your project structure
After completing this guide, your convex/ directory should look like this:
convex/
calabasas.config.ts # Your sync, events, and commands config
discord.ts # Your event handlers (you write this)
discord.generated.ts # Generated types and wrappers (don't edit)
schema.ts # Your Convex schema with calabasasTables
calabasas/
schema.ts # Generated sync table definitions (don't edit)
sync.ts # Generated sync mutations (don't edit)
_generated/ # Convex system files (auto-generated)