Skip to main content

System overview

Volvox.Bot is a Node.js 22 application built on discord.js v14. It connects to Discord’s gateway, handles events through a module system, persists state in PostgreSQL, caches hot data in Redis, and exposes a REST + WebSocket API consumed by a Next.js web dashboard.

Components

Entry point — src/index.js

src/index.js is the single entry point for the bot process. On startup it:
  1. Imports the Sentry SDK first (before all other modules) so it can instrument them.
  2. Initialises the database pool via initDb() and Redis via initRedis().
  3. Loads configuration from PostgreSQL (falling back to config.json) via loadConfig().
  4. Registers config change listeners for hot-reload.
  5. Starts background services: conversation cleanup, triage, tempban scheduler, warning expiry, announcement scheduler, and GitHub feed.
  6. Loads all slash commands from src/commands/ into a discord.js Collection.
  7. Calls client.login() to authenticate with Discord.
  8. Starts the REST API server on BOT_API_PORT (default 3001).
The discord.js client is initialised with all required gateway intents and a global allowedMentions: { parse: ['users'] } guard that prevents the bot from ever producing @everyone or @here pings, even if AI-generated content contains them.

Feature modules — src/modules/

Each module owns a discrete feature area and exports start/stop lifecycle functions called from src/index.js.
ModuleResponsibility
modules/ai.jsPer-channel conversation history, Claude completions, feedback tracking
modules/triage.jsTwo-step message classifier (fast Haiku pass → Sonnet responder)
modules/moderation.jsWarn/kick/ban/tempban/timeout, tempban scheduler
modules/warningEngine.jsWarning expiry scheduler, escalation evaluation
modules/events.jsCentral event handler registration (messageCreate, interactionCreate, etc.)
modules/config.jsConfig load/save, live /config command integration
modules/welcome.jsDynamic welcome messages with template variable substitution
modules/scheduler.jsScheduled announcement delivery
modules/botStatus.jsRotating bot presence messages
modules/memory.jsLong-term user memory via mem0
modules/githubFeed.jsGitHub event feed to a Discord channel
modules/roleMenuTemplates.jsBuilt-in reaction-role menu template seeding
modules/optout.jsPer-user opt-out preferences for memory features

Slash commands — src/commands/

Each file in src/commands/ exports a data object (a discord.js SlashCommandBuilder) and an execute(interaction) async function. Commands are loaded at startup by src/utils/loadCommands.js and registered with Discord’s API by src/deploy-commands.js. Command access is controlled by the permissions.allowedCommands map in config.json, which maps command names to one of "everyone", "moderator", or "admin".

REST API + WebSocket — src/api/

An Express 5 server starts after the bot logs in. It provides:
  • /api/v1/health — health check endpoint (also used by the Docker HEALTHCHECK directive)
  • /api/v1/config — read and write bot configuration (gated by BOT_API_SECRET)
  • /api/v1/audit-log — paginated audit log with filtering
  • /api/v1/analytics — message activity, command usage, AI feedback metrics
  • /api/v1/logs — streamed log access via WebSocket
  • /api/v1/auth/discord — Discord OAuth2 flow for dashboard login
Config keys that can be updated via the API are explicitly whitelisted in src/api/utils/configAllowlist.js. Keys not on the allowlist are rejected to prevent arbitrary config mutation from the dashboard.

Web dashboard — web/

A Next.js application that communicates with the bot’s REST API using a shared BOT_API_SECRET. It provides:
  • Dashboard overview — server stats, bot health, recent activity
  • Configuration editor — live config editing backed by /api/v1/config
  • Analytics — message activity, voice time, AI feedback charts, PDF export
  • Audit log — complete mod action history with CSV/JSON export and WebSocket streaming
  • Conversation viewer — browse and search AI conversation history
Authentication uses Discord OAuth2 via NextAuth.js. Page metadata is wired through web/src/lib/page-titles.ts using createPageMetadata().

Data stores

StorePurpose
PostgreSQL 17Config, conversation history, audit log, analytics, scheduled announcements, warnings, restart tracking
Redis 7Config cache, Discord API response cache, rate-limit counters, session storage
Redis is optional — the bot degrades gracefully to in-memory caching if REDIS_URL is not set. PostgreSQL is required for persistence; without DATABASE_URL, the bot runs from config.json only with no state persistence.

Event flow

Every Discord event follows the same path through the system: modules/events.js registers all discord.js event listeners and routes each event to the appropriate module handler. Slash command interactions (interactionCreate) are handled by looking up the command name in client.commands and calling its execute() function.

AI chat data flow

When a user mentions the bot in a channel:
  1. messageCreate fires → events.js routes to the AI handler in modules/ai.js.
  2. The handler checks whether the channel is on the ai.blockedChannelIds list. If so, it silently ignores the message.
  3. It loads the per-channel conversation history (up to ai.historyLength messages, within ai.historyTTLDays).
  4. Optionally, up to memory.maxContextMemories long-term memories are retrieved from mem0 and prepended to the context.
  5. The assembled context is sent to Claude using the Anthropic SDK. The system prompt is defined in config.ai.systemPrompt.
  6. The response is streamed back to Discord and appended to the conversation history.
  7. The updated history is persisted to PostgreSQL.
  8. If ai.feedback.enabled is true, thumbs-up/down reaction listeners are registered on the response message and feedback is recorded for dashboard analytics.

Moderation data flow

For the AI triage / auto-moderation path:
  1. Every non-bot message in monitored channels is buffered by modules/triage.js.
  2. On each tick (triage.defaultInterval ms), buffered messages are sent to the classifier model (triage.classifyModel, default claude-haiku-4-5) with a low per-message budget (triage.classifyBudget).
  3. If the classifier flags a message as needing a response or moderation action, it is passed to the responder model (triage.respondModel, default claude-sonnet-4-6).
  4. The responder either posts a reply in the channel or triggers a moderation action (warn, delete, flag to triage.moderationLogChannel).
  5. All moderation actions are written to the audit log in PostgreSQL and optionally posted to moderation.alertChannelId.
For manual moderation commands (/warn, /ban, /kick, etc.):
  1. The slash command handler validates the executor’s permissions against permissions.allowedCommands.
  2. Protected roles (moderation.protectRoles) are checked — admins and mods cannot be actioned.
  3. The action is executed on Discord (kick, ban, timeout, etc.).
  4. A DM notification is sent to the target user if moderation.dmNotifications.<action> is true.
  5. The event is written to the PostgreSQL audit log and streamed to active WebSocket connections in the dashboard.

Project structure

src
index.js
deploy-commands.js
db.js
redis.js
logger.js
sentry.js
modules
ai.js
triage.js
events.js
moderation.js
config.js
welcome.js
migrations
config.json
docker-compose.yml
Dockerfile
package.json

Build docs developers (and LLMs) love