Skip to content

Plugins

Plugins are the primary extension mechanism in Popii. They tap into the client lifecycle, augment the pop context with new methods, register slash commands, add web routes, and more — all from a single plugins: [] array in your config.

import {
popiiClient,
sqlitePlugin,
webPlugin,
errorHandlerPlugin,
voicePlugin,
} from "popii-framework";
const client = popiiClient({
token: process.env.DISCORD_TOKEN!,
plugins: [
sqlitePlugin(), // must come before plugins that depend on it
voicePlugin(),
webPlugin({ port: 3000 }),
errorHandlerPlugin(),
],
});
PluginPurpose
sqlitePluginBun SQLite database in every pop
mongoosePluginMongoDB via Mongoose
PluginPurpose
levelsPluginPer-guild XP and level ranking
economyPluginVirtual currency, daily rewards, leaderboard
gamesPluginCoinflip, slots, RPS, and blackjack
moderationPluginWarn, kick, ban, timeout, case history
voicePluginMusic playback, queue, SponsorBlock
deskPluginSupport ticket system
giveawayPluginTimed giveaways
pollPluginButton-based polls
starboardPluginStar-reaction highlights
welcomePluginWelcome and goodbye messages
autorolePluginAuto-assign roles on join
rssPluginRSS/Atom feed posting
notifyPluginTwitch stream and YouTube upload alerts
remindersPluginUser-set time-based reminders
snapshotsPluginPeriodic server activity snapshots
lastFmPluginLast.fm scrobbling and stats
PluginPurpose
webPluginHTTP server, dashboard, OAuth2
uiPluginPagination, modals, wizards, prompts
popiiAiPluginAI text generation (OpenAI, Gemini, Anthropic)
autoModPluginAI-powered message moderation
payPluginPatreon and Ko-fi webhooks
captchaPluginhCaptcha verification
canvasPluginImage generation
activityRotatorPluginRotating bot status
webhookPluginInbound webhooks with HMAC validation
gdprPluginUser data deletion command
PluginPurpose
errorHandlerPluginUser-friendly error replies
permissionGuardPluginEnforce command permission fields
commandLoggerPluginExecution logging
commandAnalyticPluginIn-memory usage counters
telemetryPluginError capture with Sentry/Datadog support
reloadPluginOwner-only /reload command
sandboxPluginIsolated test environment
pluginManagerPluginRuntime plugin management

Install from the registry with the CLI:

Terminal window
bunx popii add economy # installs popii-plugin-economy
bunx popii search music # search the registry

Or enable auto-discovery to automatically load any installed popii-plugin-* package:

const client = popiiClient({
token: process.env.DISCORD_TOKEN!,
autoDiscover: true,
pluginConfig: {
"popii-plugin-economy": { currencySymbol: "🪙" },
},
});

A plugin is a plain object with a name and optional lifecycle hooks:

import type { PopiiPlugin } from "popii-framework";
export function analyticsPlugin(options: { dsn: string }): PopiiPlugin {
return {
name: "analytics",
setup(client) {
// Called once before Discord connects.
// Register listeners, initialize state, create database tables.
client.global.on("xp-gained", ({ userId, amount }) => {
trackEvent("xp_gained", { userId, amount });
});
},
ready(client) {
// Called after the bot is online.
// Safe to access client.discord.guilds.cache here.
console.log(`Analytics active for ${client.discord.guilds.cache.size} guilds`);
},
cleanup(client) {
// Called on graceful shutdown.
// Flush buffers, close connections.
},
};
}
HookWhenNotes
setup(client)Before Discord connectsSafe to register listeners; client.discord not yet ready
ready(client)After Discord connectsclient.discord.guilds.cache is populated
reload(client)After a hot-reloadRe-apply middleware wrapping if needed
cleanup(client)On graceful shutdownClear intervals, flush buffers, close sockets
onCommandExecute(pop, cmd)Before every commandGood for analytics and logging
onCommandError(err, pop)When a command throwsGood for error reporting services
onContextCreate(pop)When a pop context is createdInject data into context before the handler runs

Plugins can bundle their own slash commands. The framework registers them after user commands are loaded, and user commands always take priority — a file at src/commands/greet.ts silently overrides any plugin command named greet.

import type { PopiiPlugin } from "popii-framework";
export function supportPlugin(): PopiiPlugin {
return {
name: "support",
commands: [
{
name: "ticket",
description: "Open a support ticket",
async do(pop) {
await pop.reply("Opening your ticket...");
},
},
],
// setup() is optional for command-only plugins
};
}

Letting bot owners customize plugin commands

Section titled “Letting bot owners customize plugin commands”

Users can suppress or patch individual plugin commands from the top-level config without touching the plugin source:

const client = popiiClient({
plugins: [supportPlugin()],
pluginCommands: {
disable: ["ticket"], // don't register this command
overrides: [
{ name: "ticket", description: "Need help? Open a ticket." }, // patch a field
],
},
});

The plugin’s do handler is preserved in overrides — only the declared fields are replaced.

Declare a settingsSchema to add fields to the web dashboard’s per-server settings form automatically:

export function greetPlugin(): PopiiPlugin {
return {
name: "greet",
settingsSchema: [
{
id: "greetChannel",
label: "Greeting Channel",
type: "channel",
description: "Where to post welcome messages",
category: "Greet Plugin",
},
{
id: "greetEnabled",
label: "Enable greetings",
type: "boolean",
default: true,
category: "Greet Plugin",
},
],
setup(client) { /* ... */ },
};
}
  1. Clone the plugin template
  2. Name your package popii-plugin-<name>
  3. Add a "popii" metadata block to package.json:
{
"name": "popii-plugin-soundboard",
"popii": {
"displayName": "Soundboard",
"description": "Play sound effects in voice channels",
"category": "voice",
"export": "soundboardPlugin"
}
}