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 response

A basic command that responds with "Pong!" — your hello world for Discord bots.

/server-infoSynced data

Queries 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

1

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:

Server Members Intent — needed for member sync and the /server-info command
Message Content Intent — needed if you plan to read message content later

Keep your bot token secret. Never commit it to source control or share it publicly. Calabasas encrypts it before storage.

2

Login to Calabasas

Authenticate the CLI with your Calabasas account. This opens your browser to complete the OAuth flow.

npx calabasas login

Your API key is saved to ~/.calabasas/config.json.

3

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 init

This creates convex/calabasas.config.ts with a starter template.

4

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.

convex/calabasas.config.ts
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.

5

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 add

The 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.

6

Generate type-safe handlers

This reads your config and generates TypeScript types, validators, and handler wrappers tailored to your setup.

npx calabasas generate

This creates several files:

convex/discord.generated.ts— Event handler wrapper and TypeScript types
convex/calabasas/schema.ts— Sync table definitions
convex/calabasas/sync.ts— Sync mutations for the gateway
7

Add 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.

convex/schema.ts
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.

8

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.

convex/calabasas/discord.ts
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.ApplicationCommand

Discord 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 ApplicationCommandData

After 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!

9

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 Variables

You 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.

10

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 push

The 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.

11

Invite the bot to your server

Go back to the Discord Developer Portal and generate an invite link.

  1. Go to your application → Installation
  2. Under Guild Install, add the bot and applications.commands scopes
  3. Select the permissions your bot needs (at minimum: Send Messages and Use Slash Commands)
  4. Copy the install link and open it in your browser
  5. Select your server and authorize
12

Test your bot!

Make sure your Convex dev server is running (npx convex dev), then head to your Discord server and try:

/ping

Pong!

/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)