Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture Overview

discord-bot-rs is built from small, well-isolated modules communicating through a central shared Data struct and a handful of Discord event handlers. The process is a single Tokio runtime that hosts a Discord client, a Postgres connection pool, and an embedded MCP server, plus whichever feature modules you’ve turned on in config.toml. Everything else — music players, game state, rate limiters, the AI pipeline — hangs off Data and is accessed async-safely through per-guild or per-channel maps. This page sketches the high-level shape; the rest of the architecture section drills into each piece.

Components

graph TB
    subgraph "Bot Process"
        Gateway[Discord Gateway<br/>serenity shard]
        Handler[Event + Command Handler<br/>poise]
        Commands[Commands<br/>src/commands/]
        AI[AI Pipeline<br/>src/ai/]
        Music[Music Player<br/>src/music/]
        Games[Games<br/>wordle, connections, stocks]
        DB[(PostgreSQL<br/>sqlx pool)]
        MCP[MCP Server<br/>src/mcp + axum]
    end
    Discord[Discord API] <--> Gateway
    Gateway --> Handler
    Handler --> Commands
    Handler --> AI
    Handler --> Music
    Commands --> DB
    AI --> DB
    Games --> DB
    Handler --> Games
    Claude[MCP client<br/>e.g. Claude Code] --> MCP
    MCP --> Handler

One Tokio process hosts a serenity shard (the WebSocket to Discord’s gateway), a poise dispatcher wrapped around it, and an axum-based MCP server bound on a local port. Gateway events flow into the event handler, which either fires a command (via poise) or routes the event directly to a feature module — the AI pipeline for @mentions and replies, the music player for voice state updates, the games module for interaction buttons. Every module talks to the Postgres pool through sqlx. The MCP server is a separate ingress point: it exposes tools like list_guilds and send_message to outside clients, and its handlers reach into the same shared state the event handler uses.

The Data struct

Poise gives every handler a typed reference to a user-defined state struct. In this project that struct is Data, defined at the top of src/main.rs. It holds:

  • db: sqlx::PgPool — the shared Postgres connection pool.
  • http_client: reqwest::Client — a preconfigured HTTP client for yt-dlp, DeepSeek, Gemini, Finnhub, DuckDuckGo scraping, and any other outbound HTTP work a feature needs.
  • config: Config — loaded from environment variables at startup.
  • personality: String and bot_name: String — per-instance identity.
  • Optional feature configsauto_role_config, minecraft_config, join_role_config, welcome_config, etc. Each is Option<T> so that disabled features simply hold None.
  • Per-guild state maps: guild_players, track_handles, now_playing_msgs, idle_timers, connections_games, wordle_games. All are Arc<DashMap<key, value>>, giving lock-free guild lookup. Most values are wrapped in Arc<Mutex<T>> for serialised access inside one guild; track_handles is the exception — it stores TrackHandle directly because songbird’s handle is already cheap to clone and internally synchronised.
  • rate_limiters: RateLimiters — sliding-window limiters for AI, music, moderation, stock tools, and the welcome/join flow, keyed by user ID. All five are enforced; a periodic cleanup task in main.rs evicts empty buckets every 5 minutes. See Concurrency Model.
  • mcp_started: AtomicBool and started_at: DateTime<Utc> — one-shot flags used to guard the MCP server against gateway reconnects and to let the AI history builder ignore messages from previous bot lifetimes.

A single Arc<Data> is cloned into every command context, event handler future, and background task. Cheap to clone, shared everywhere, no global state — see Concurrency Model for why this shape works without contention.

Per-instance model

Each running bot is a separate Linux process with its own Data, its own Discord token, its own Postgres schema, and its own instance config directory. “Multi-tenant” here means “run two containers with two .env files,” not “one process with guild-scoped data.” The Multi-Instance Model page explains the boundaries and why the project chose a schema-per-instance approach over the alternatives.

How events flow

Discord pushes an event down the gateway. serenity parses it into a typed variant and hands it to poise’s event dispatcher, which either matches a prefix command (the !m family) and runs the command handler, or calls the plain event_handler for everything else. The event handler is one big match over FullEvent variants — ready, message create, voice state update, interaction create, member add — and each arm dispatches to the corresponding feature module. Responses go back to Discord via serenity’s HTTP client. The full lifecycle is in Data Flow.

Major modules

ModuleResponsibility
src/main.rsEntry point, Data struct, framework init, background task spawning
src/config.rsEnvironment variable loading
src/instance_config.rsconfig.toml parsing for per-instance feature flags
src/error.rsBotError enum and From conversions
src/commands/Every prefix command, all parented under !m
src/events/Gateway event dispatcher, message handler, voice-state handler
src/ai/DeepSeek/Gemini pipeline, tool execution, response sanitising
src/music/Per-guild player, yt-dlp + songbird pipeline, voice handling
src/wordle/Wordle game state and puzzle fetching
src/connections/NYT Connections game state and puzzle fetching
src/stocks/Virtual stock trading, Finnhub integration
src/minecraft/Minecraft link verification, donator sync, chargeback webhooks
src/autorole.rsTime/message-based role promotion
src/mcp/Embedded MCP server and tool definitions
src/db/Connection pool, models, query helpers
src/util/Rate limiters, duration parsing

Tech choices

  • serenity — Rust’s mature Discord library, the foundation for everything else. Chosen for its stable gateway handling and typed model objects.
  • poise — a command framework built on serenity. Used for its prefix-command parsing, subcommand tree, and typed Context<'_, Data, BotError>. Saves hundreds of lines of boilerplate compared to raw serenity.
  • songbird — the voice driver. Handles voice gateway, UDP, and Opus packet assembly so this project only has to feed it audio bytes.
  • sqlx — async Postgres client with compile-time-checked queries. Chosen over an ORM for explicitness and because the schema is small enough not to need one.
  • dotenvy — reads .env at startup. Modern maintained fork of the classic dotenv crate.
  • rmcp — the official Rust SDK for the Model Context Protocol; used for the embedded MCP server.
  • axum — HTTP server. The MCP server and (optionally) chargeback webhook router run on axum inside the same Tokio runtime as the Discord client.
  • dashmap — a lock-free concurrent hash map. Used for every per-guild and per-channel state map so that work in one guild never blocks work in another.

Where to go next

  • Multi-Instance Model for the process and schema layout when you run more than one bot against one Postgres.
  • Data Flow for the step-by-step lifecycle of a single Discord event.
  • AI Pipeline for how @mention → response actually works, including tool-use loops.
  • Music Pipeline for the yt-dlp + songbird path.
  • Concurrency Model for DashMap + tokio::Mutex patterns and why locks are the last tool, not the first.
  • Error Handling for how BotError reaches users.
  • Database Schema for every table and what owns it.