Skip to content
VORQUL
VorqulBOT / Platform

Plugin system

A modular bot that requires forking the core to add one command is not modular. The plugin system exists so that server-specific logic stays server-specific.

VorqulBOT supports drop-in plugins. A plugin is a folder under plugins/ containing a manifest.json and a JavaScript entry file (.js or .mjs, never .ts) that default-exports an object with its own commands, events and optional onLoad and onUnload hooks. Plugins load at startup, before the bot logs in, so their commands register with Discord together with the built-in ones, and they require no modification to the bot core.

Language
JavaScript (ESM); compile TypeScript yourself
Manifest
manifest.json
Entry point
.js or .mjs, default-exported object
Loaded
At startup, before login

Anatomy of a plugin

The manifest holds six fields: name, version, author, description, main and enabled. The file named by main must be a .js or .mjs file inside the plugin folder. A .ts entry point is rejected, because the bot runs compiled JavaScript. If you prefer TypeScript, compile it to .js before shipping.

The entry file default-exports one object. Everything except name is optional, so a plugin may add only commands, only events, or neither. A plugin that adds one command is a manifest, an index file and a single command definition, and the loader picks it up from the plugins folder on start.

plugins/
  my_plugin/
    manifest.json
    index.js

// manifest.json
{
  "name": "my_plugin",
  "version": "1.0.0",
  "author": "You",
  "description": "What it does",
  "main": "index.js",
  "enabled": true
}

// index.js
import { SlashCommandBuilder } from 'discord.js';

let ctx;

export default {
  name: 'my_plugin',
  version: '1.0.0',
  commands: [
    {
      data: new SlashCommandBuilder().setName('ping').setDescription('Reply with pong'),
      category: 'plugin',
      execute: async (interaction) => {
        await interaction.reply({ embeds: [ctx.embed.success('Pong', 'Still here.')] });
      }
    }
  ],
  events: [],
  onLoad: (context) => { ctx = context; },
  onUnload: () => {}
};

What a plugin gets handed

onLoad and onUnload receive a plugin context rather than the bare client. It carries the discord.js client, the embed helper the rest of the bot uses, a namespaced logger, and the translation function t(key, guildId) so a plugin can speak all nine interface languages without shipping its own i18n.

It also carries a private data directory plus readData and writeData helpers, so a plugin that needs to remember a counter or a list does not have to bring a database or reach into the bot storage. Do not import anything from src/ or dist/ - those paths differ between development and a deployed build, and the context object exists so you never need to.

  • client - the discord.js client
  • embed - the same embed helper the core uses
  • logger - info, warn, error and debug, namespaced to the plugin
  • t(key, guildId, vars) - translated strings
  • dataDir, readData, writeData - the plugin private storage

Shared module registry

The bot and the dashboard read one module registry, the 44 keys in src/shared/moduleRegistry.ts. That means a toggle cannot exist in the dashboard without the bot honouring it.

This is the detail that keeps the two halves from drifting apart over time, which is the usual failure mode for a bot with a separate web UI.

Loading and unloading

Plugins load at startup, in alphabetical folder order, before the bot logs in. A bad manifest is skipped with a warning and the other plugins still load. On shutdown or unload, onUnload runs and the plugin listeners and commands are removed; a reload re-imports the file with a fresh cache key, so it picks up your edits without leaving duplicate listeners behind.

A plugin can also ship disabled: set enabled to false in the manifest and the loader will skip it until you say otherwise. The bundled sample_plugin is shipped that way on purpose. Adding or removing a command needs a restart before Discord sees the change.

Terms on this page

Module registry Webhook signature Self-hosted

Frequently asked questions

Related pages