Documentation Index
Fetch the complete documentation index at: https://mintlify.com/betterdiscord/betterdiscord/llms.txt
Use this file to discover all available pages before exploring further.
Follow these best practices to create high-quality plugins that are maintainable, performant, and compatible with other plugins.
Plugin lifecycle
Always clean up in stop()
Every modification your plugin makes should be reversed in the stop() method:
start() {
BdApi.DOM.addStyle("MyPlugin", ".my-class { color: red; }");
BdApi.Patcher.after("MyPlugin", SomeModule, "render", this.patchRender);
}
stop() {
BdApi.DOM.removeStyle("MyPlugin");
BdApi.Patcher.unpatchAll("MyPlugin");
}
Use bound API instances
Create a plugin-specific API instance for automatic cleanup tracking:
class MyPlugin {
constructor() {
// Bound to "MyPlugin"
this.api = new BdApi("MyPlugin");
}
start() {
// Automatically associated with your plugin
this.api.Patcher.after(SomeModule, "method", callback);
this.api.Data.save("settings", this.settings);
}
stop() {
// One call to unpatch everything
this.api.Patcher.unpatchAll();
}
}
Handle plugin reloading
Your plugin should work correctly when reloaded:
start() {
// Check if already started
if (this.started) {
this.api.Logger.warn("Already started, stopping first");
this.stop();
}
this.started = true;
this.loadSettings();
this.setupPatches();
}
stop() {
if (!this.started) return;
this.started = false;
this.cleanup();
}
Error handling
Wrap risky operations in try-catch
Prevent your plugin from crashing Discord:
start() {
try {
this.setupPatches();
} catch (error) {
this.api.Logger.error("Failed to setup patches:", error);
BdApi.UI.showToast("MyPlugin failed to start", {type: "error"});
}
}
Validate module existence
Check that webpack modules exist before using them:
const MessageActions = BdApi.Webpack.getModule(
BdApi.Webpack.Filters.byKeys("sendMessage")
);
if (!MessageActions) {
this.api.Logger.error("Failed to find MessageActions module");
BdApi.UI.showToast("MyPlugin is incompatible with this Discord version", {
type: "error"
});
return;
}
Graceful degradation
Provide fallbacks when features aren’t available:
const MessageModule = BdApi.Webpack.getModule(
BdApi.Webpack.Filters.byKeys("sendMessage")
);
if (MessageModule) {
// Full functionality
this.enableAdvancedFeatures();
} else {
// Basic functionality
this.api.Logger.warn("Some features unavailable");
this.enableBasicFeatures();
}
Minimize patching
Only patch what you need to modify:
// Patch only the specific function
BdApi.Patcher.after("MyPlugin", MessageComponent, "render", (_, args, ret) => {
// Minimal modification
if (ret?.props?.className) {
ret.props.className += " my-custom-class";
}
});
Cache webpack modules
Find modules once and reuse them:
class MyPlugin {
constructor() {
this.api = new BdApi("MyPlugin");
this.modules = {};
}
start() {
// Cache modules
this.modules.MessageActions = BdApi.Webpack.getModule(
BdApi.Webpack.Filters.byKeys("sendMessage")
);
this.modules.UserStore = BdApi.Webpack.Stores.UserStore;
}
someMethod() {
// Reuse cached module
const user = this.modules.UserStore.getCurrentUser();
}
}
Debounce frequent operations
Avoid excessive processing:
class MyPlugin {
constructor() {
this.api = new BdApi("MyPlugin");
this.updateDebounced = this.debounce(this.update.bind(this), 300);
}
debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
onFrequentEvent() {
// Called many times, but update() only runs once per 300ms
this.updateDebounced();
}
update() {
// Expensive operation
}
}
Compatibility
Use semantic versioning
Follow semver for version numbers:
- Major (1.0.0 → 2.0.0): Breaking changes
- Minor (1.0.0 → 1.1.0): New features, backwards compatible
- Patch (1.0.0 → 1.0.1): Bug fixes
Check Discord/BD versions
Test compatibility with different versions:
start() {
const discordVersion = BdApi.version;
if (!this.isCompatible(discordVersion)) {
BdApi.UI.showNotice(
`MyPlugin may not work correctly with BetterDiscord ${discordVersion}`,
{type: "warning", timeout: 0}
);
}
}
isCompatible(version) {
const [major, minor] = version.split(".").map(Number);
return major >= 1 && minor >= 8;
}
Avoid conflicts with other plugins
Use unique identifiers:
// Prefix classes and IDs
BdApi.DOM.addStyle("MyPlugin", `
.myplugin-container { /* ... */ }
#myplugin-settings { /* ... */ }
`);
// Namespace data keys
this.api.Data.save("myplugin:settings", settings);
this.api.Data.save("myplugin:cache", cache);
Code quality
Use consistent naming
Follow JavaScript conventions:
class MyPlugin {
// PascalCase for classes
// camelCase for methods and variables
loadSettings() { }
saveSettings() { }
// UPPER_CASE for constants
static DEFAULT_TIMEOUT = 5000;
static API_ENDPOINT = "https://api.example.com";
}
Document your code:
/**
* Processes a message and applies transformations
* @param {Object} message - The message object
* @param {string} message.content - Message text content
* @param {string} message.author - Author ID
* @returns {Object} Transformed message object
*/
processMessage(message) {
// Implementation
}
Use named constants:
class MyPlugin {
static TOAST_DURATION = 3000;
static MAX_RETRIES = 3;
static DEBOUNCE_DELAY = 300;
start() {
BdApi.UI.showToast("Started", {
timeout: MyPlugin.TOAST_DURATION
});
}
}
Security
Never trust user input:
updateSetting(key, value) {
// Validate the key
const allowedKeys = ["theme", "volume", "enabled"];
if (!allowedKeys.includes(key)) {
this.api.Logger.warn(`Invalid setting key: ${key}`);
return;
}
// Validate the value
if (key === "volume" && (value < 0 || value > 100)) {
this.api.Logger.warn(`Invalid volume: ${value}`);
return;
}
this.settings[key] = value;
this.saveSettings();
}
Sanitize HTML content
Prevent XSS attacks:
const {React} = BdApi;
// Good: React automatically escapes
React.createElement("div", {}, userContent);
// Bad: Direct HTML insertion
document.getElementById("content").innerHTML = userContent;
// If you must use innerHTML, sanitize first
const sanitized = userContent
.replace(/</g, "<")
.replace(/>/g, ">");
document.getElementById("content").innerHTML = sanitized;
Be careful with eval
Avoid eval() and Function() constructor:
// Never do this
const code = this.api.Data.load("userCode");
eval(code); // DANGEROUS!
// Use safer alternatives
const config = JSON.parse(this.api.Data.load("config"));
Debugging
Use meaningful log messages
Help users troubleshoot:
start() {
this.api.Logger.info("Starting MyPlugin v1.2.3");
const module = BdApi.Webpack.getModule(filter);
if (!module) {
this.api.Logger.error(
"Failed to find required module.",
"This may indicate a Discord update.",
"Please check for plugin updates."
);
return;
}
this.api.Logger.info("Module found, applying patches");
this.setupPatches();
this.api.Logger.info("MyPlugin started successfully");
}
Add debug mode
Allow verbose logging:
class MyPlugin {
start() {
this.settings = this.api.Data.load("settings") || {};
}
debug(...args) {
if (this.settings.debugMode) {
this.api.Logger.info("[DEBUG]", ...args);
}
}
someMethod() {
this.debug("someMethod called with args:", arguments);
// Implementation
}
}
Distribution
Provide clear installation instructions
Include a README with:
- What the plugin does
- Installation steps
- Configuration options
- Known issues
- Support contact
Use update URLs
Allow automatic updates:
/**
* @name MyPlugin
* @version 1.2.3
* @updateUrl https://raw.githubusercontent.com/user/repo/main/MyPlugin.plugin.js
*/
Include changelog in updates
Show users what changed:
start() {
const lastVersion = this.api.Data.load("version");
const currentVersion = "1.2.3";
if (lastVersion !== currentVersion) {
this.showChangelog(currentVersion);
this.api.Data.save("version", currentVersion);
}
}
showChangelog(version) {
BdApi.UI.showChangelogModal({
title: "MyPlugin Updated",
subtitle: `Version ${version}`,
changes: [
{
title: "New Features",
type: "added",
items: [
"Added custom themes",
"Added keyboard shortcuts"
]
},
{
title: "Bug Fixes",
type: "fixed",
items: [
"Fixed crash on startup",
"Fixed settings not saving"
]
}
]
});
}