Clean starting point for building an external (npm-installable) plugin for Advanced Discord Bot (ADB).
See adb-plugin-reminders (sibling repo) for a complete, working example built from this template.
- Copy this folder / use as a GitHub template repo, rename it to
adb-plugin-<your-name>. - Find-and-replace
adb-plugin-templatewith your real package name inplugin.json,package.json, source, and tests. - Naming rule: the package name (and the folder name, if run as a local plugin) must start with
adb-plugin-— that's the exact stringPluginManagerscansnode_modules/for. - Implement your feature in
index.js/commands//models/. - Update this README.
This template stays isolated by default and uses APIs common to both supported load modes. Worker isolation means plugin resource operations go through Core's capability-gated RPC; it is not a reason to claim that all npm libraries fail to resolve or that a Node worker alone is a complete security sandbox.
- Worker:
ctx.clientand the command handler'sclientargument arenull.ctx.discordprovides Discord operations through RPC. Returned Discord data and event arguments are serialized values, not discord.js class instances. - Direct: the client is available, but
ctx.discorddoes not exist. Background message delivery or scheduling needs a deliberate narrow mode adapter, as inadb-plugin-reminders/lib/runtime.js. Do not declareraw-clientjust to hide an incompatible scheduler signature or unsupported model operation. - Schemas are real
mongoose.Schemainstances.models/example.jsrequires Mongoose to construct a schema without opening a connection.ctx.defineModelregisters a namespaced model; workers serialize supported scalar fields, defaults, validation options, and indexes for Core to compile. Do not pass a raw definition or compiled model, and do not assume arbitrary custom schema functions survive RPC. - Use plain message/embed objects and shared model operations. In particular,
updateOneworks in both modes; staticModel.saveis a worker facade, not a Mongoose model method. Current workers also attachsave/markModifiedfacades to non-lean documents, but those documents are not Mongoose instances. - Declare capabilities and handle errors. Resource RPC calls reject when a required capability is missing. Await registration and operations rather than discarding their failures. Do not import bot internals, connect to Mongo yourself, or read secrets from the process environment.
Treat ctx as read-only except for its predeclared models field. Direct
PluginContext seals that shape and makes other fields non-writable; the current
worker shim does not enforce the same object immutability. Neither fact permits
mutating the runtime context as a plugin integration technique.
Every plugin's entry file (default index.js) must export:
async function load(ctx) { /* ... */ }
module.exports = { load };PluginManager calls load(ctx) once at startup (or on hot-reload). Errors thrown here disable just this plugin — they don't crash the bot.
Commands have the shape { data, execute(interaction, client) }. A factory captures
the model and ctx.db from load; the second execute argument is never ctx.
This example implements /example [text]: it stores and echoes nonblank text up to
2000 characters with mentions disabled. Without text it reads the current guild's
config.data.welcomeMessage, falling back to the documented greeting. DMs are
rejected before storage. The member-join handler only logs for enabled guilds.
| Member | What it is |
|---|---|
ctx.client |
Direct client only; null in workers. Not used by this example. |
ctx.discord |
Worker only: sendToChannel(channelId, payload), sendDM(userId, payload), getGuild(guildId), getMember(guildId, userId), fetchChannel(channelId). Never assume it exists in direct mode. |
ctx.db |
Shared config API: getPluginConfig(guildId, name), updatePluginConfig(guildId, name, data), getAllPluginConfigs(guildId). Worker access needs storage:own-collection; Core scopes own-config access to the calling plugin. |
ctx.registerCommand(command) |
Register { data, execute(interaction, client) }. Await it; registration is asynchronous in workers. |
ctx.registerEvent(eventName, handler, options?) |
Listen to a Discord event. Direct mode supplies native event arguments; Core forwards serialized arguments to workers, followed by a null client. Validate payload shape and handle async failures. |
ctx.defineModel(modelName, schema) |
Register a real Schema as plugin_<your-plugin-name>_<modelName>. Worker operations wait for schema registration. |
ctx.hooks.on(hookName, handler) / ctx.hooks.emitHook(hookName, payload) |
Subscribe/emit with hooks:subscribe / hooks:emit. on returns an unsubscribe function; worker hooks do not provide direct HookBus priority, onAny, or return-value merging. |
ctx.scheduler (direct) |
schedule(name, expression, callback) returns a cron task; remove by unschedule(name). Names share a scheduler, so prefix them with your plugin name. |
ctx.scheduler (worker) |
await schedule(expression, callback, name) returns Core's task ID; await cancel(taskId) removes it. Requires scheduler:cron, which this template does not declare or use. |
ctx.config.env |
Worker environment grants supplied by Core (normally empty); direct mode receives the host config. Do not assume ambient secrets are available. |
ctx.logger |
.info() / .warn() / .error(), namespaced to your plugin |
ctx.overrideCommand() works only in direct mode; workers warn and do nothing.
Use ctx.registerCommand() for portable commands. ctx.commands is not a
registration API and is null in workers; there is no ctx.events namespace.
Shared model operations include find().sort().skip().limit().lean(), findOne,
create, countDocuments, updateOne, and deleteOne/deleteMany. updateOne
returns counts, not the updated document. Like Mongoose, it does not run schema
validators by default, so validate user-editable input in command handlers.
Plugin config rows have a top-level enabled flag and a data object. A missing
row is disabled. updatePluginConfig(..., data) replaces the entire data object,
not the enable flag. Read config.enabled === true before per-guild scheduled
side effects, including DMs: command/event routing does not automatically gate
an independent cron callback. Settings declarations alone do not implement
behavior: this example consumes welcomeMessage; other sample settings need
handlers before they can enforce limits, select modes, or send announcements.
| Field | Required | Notes |
|---|---|---|
name |
yes | must start with adb-plugin- |
version |
yes | semver |
description / author |
yes | |
main |
no | defaults to index.js |
displayName |
no | shown in marketplace UI |
requiresRestart |
no | true disables hot-reload eligibility |
isolation |
no | true (default) run in a worker |
manifestVersion |
v2 | set to 2 |
process |
v2 | { model: "pooled"|"persistent"|"oneshot", maxExecutionMs, memoryMb, persistentReason } |
capabilities |
yes for isolated | what the broker lets you do — { storage: [...], discord: [...], hooks: [...], scheduler: [...] } |
permissions |
v2 | mirror of capabilities + network.outbound host allowlist, filesystem, childProcess, nativeAddons |
configSchema |
no | JSON Schema → per-guild settings UI, read via ctx.db.getPluginConfig(guildId, name) |
discordPermissions |
no | Discord permission flags for the bot invite link |
npm install
npm testtest/local-harness.js loads the plugin in both mock modes and calls the registered
handler with execute(interaction, client), using the direct client or worker
null, never the plugin context. createInteraction supplies nested option data
and the matching getters. Reply validation enforces content (2000), description
(4096), and total embed text (6000) limits.
test/mock-ctx.js is a standalone contract double for the APIs exercised here,
not a complete Discord/Mongoose implementation. It uses real Mongoose casting,
defaults, validation, and ObjectIds over in-memory rows; implements query ordering,
pagination, date predicates, and scoped updates; distinguishes direct/worker
document and scheduler shapes; and checks declared worker capabilities. Config
updates preserve the enable flag and replace only data. Tests never connect to
Mongo, Discord, or another live service and never expose the runner's environment.
runTask invokes a registered callback, not Core's cron event transport. These
tests also do not execute Core's RPC interaction bridge or schema serializer.
Core must forward replies/options and route scheduler events by the returned
task ID correctly. Extend tests when using additional APIs, and verify the real
worker/broker path separately before deployment. Copy the mock into new plugin
repos; do not introduce runtime or test imports into sibling repositories.
- Have a working local checkout of Advanced Discord Bot.
- Symlink or copy your plugin folder into its
plugins/directory:or, to test the actualln -s $(pwd) /path/to/Advanced-Discord-Bot/plugins/adb-plugin-yournamenode_modules/adb-plugin-*discovery path a real npm install would use:npm link cd /path/to/Advanced-Discord-Bot && npm link adb-plugin-yourname
- Start the bot, confirm your plugin's load-log line appears.
- If you added slash commands, run
npm run deployin the bot repo — command logic hot-reloads, but Discord command registration needs an explicit deploy. - Exercise the feature for real in a Discord server.
npm login
npm publishAnyone installs it with npm install adb-plugin-yourname into their bot's root — ADB's PluginManager auto-discovers any node_modules/adb-plugin-* folder containing a plugin.json.
See REGISTRY-SETUP.md in the main ADB repo — fork the registry repo, add an entry to plugins.json with your npmPackage name, open a PR.
This project is licensed under the GNU Affero General Public License v3.0. See the LICENSE file for details.
This repository follows the policies of the main ADB project.
- Contribution Guidelines: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- Security Policy: SECURITY.md