Overview
Architecture, lifecycle, directory layout, and core exports for building Medusa modules.
Everything Medusa executes at runtime lives inside a module. All native features (starting with the core module) run as modules. Custom modules load identically, receive the identical runtime primitives, and have full access to Medusa's database, Discord gateway, HTTP router, web dashboard, audit logs, and localization systems.
This documentation is the complete reference for building modules. For bot deployment and server configuration instructions, refer to the Setup Guide and Configuration Guide.
Module Architecture
Each directory located directly in modules/ represents an isolated module. On boot, Medusa scans the directory, imports main.js, constructs the exported module class, and recursively registers and loads every subsystem located within src/ and resources/.
modules/
example/
main.js
resources/
cards/
config/
dashboard/
pages/
emojis/
lang/
leaderboards/
logs/
nodes/
placeholders/
posts/
src/
clocks/
commands/
events/
handlers/
models/
routes/Loading Lifecycle
Medusa initializes modules only after the internal API is listening and the dashboard reports ready. If either fails to start, boot stops before any module registers. Modules then load in two strict phases:
- Registration Phase: Medusa verifies licensing, installs any missing
dependencies.nodepackages, initializes schemas, loads configuration groups, language dictionaries, logging categories, leaderboard providers, post providers, automation flow nodes, database models, HTTP routes, and custom dashboard pages. - Loading Phase: Medusa initializes handlers, Discord slash and prefix commands, Discord gateway events, and scheduled clocks. Medusa then triggers
onStartup(), syncs database tables, mounts HTTP routes on the API server, and registers cron schedules.
Per-Server Modules
Every installed module loads once for the whole bot, but each server can turn modules on or off from the dashboard Modules page. Changes apply immediately, with no restart. When a module is off in a server, Medusa:
- Leaves its slash commands out of that server's command sync and blocks its prefix commands and autocomplete.
- Answers its buttons, select menus, and modals (
medusa:<component>:<module>:...ids) with the coremoduleDisabledwarning. - Skips its Discord and internal events when the payload resolves to that server (
guildId,guild, ormessage.guildId). - Hides its dashboard pages, configs, logging groups, leaderboard metrics, and
/helpentries for that server, and its routes answer404to dashboard requests scoped to that server. - Skips its automation nodes in flows running in that server.
Clocks run once for the whole bot, so a clock that loops over servers or posts into channels must check the server itself:
for (const guild of this.medusa.discord.client.guilds.cache.values()) {
if (!this.medusa.modules.enabled(this.module, guild.id)) continue;
}Dashboard requests carry the selected server in the x-guild-id header, so module routes can read it from request.headers["x-guild-id"].
When Medusa shuts down or a module is deloaded, Medusa stops all running clocks, triggers onShutdown(), unmounts routes, and closes resources cleanly.
Subsystem Layout
Medusa loads files automatically based on folder location and filename conventions:
| Directory | Loaded File Pattern | Base Class | Export Name |
|---|---|---|---|
main.js | Root module entrypoint | Modules | Module Class |
src/models/ | Every *.js file directly in folder | Models | Model Class |
src/handlers/ | Every *.js file directly in folder | Handlers | Handler Class |
src/commands/ | Every *.js file directly in folder | Commands | Command Class |
src/events/ | Every *.js file directly in folder | Events | Event Class |
src/clocks/ | Every *.js file directly in folder | Clocks | Clock Class |
src/routes/ | Every route.js file at any nested path | Routes | Route Class |
resources/config/ | Every *.js file directly in folder | Configs | Config Class |
resources/logs/ | Every *.js file directly in folder | Logs | Log Class |
resources/lang/ | English classes in en/*.js, translations in translations.json | Langs | Lang Class |
resources/leaderboards/ | Every *.js file directly in folder | Leaderboards | Leaderboard Class |
resources/posts/ | Every *.js file directly in folder | Posts | Post Class |
resources/placeholders/ | Every *.js file directly in folder | Placeholders | Placeholder Class |
resources/nodes/ | Every *.js file directly in folder | Nodes | Node Class |
resources/dashboard/pages/ | Every page.js file at any nested path | Pages | Page Class |
resources/emojis/ | Every image file (.png, .gif, etc.) and optional emojis.json | Image / JSON | Emojis |
Base Module Class
Every module entrypoint (modules/<name>/main.js) must export a default or named class extending Modules imported from #medusa/modules.
import { Modules } from "#medusa/modules";
export class ExampleModule extends Modules {
constructor(medusa) {
super(medusa, {
name: "example",
displayName: "Example",
icon: "puzzle",
author: "Zero Development",
version: "1.0.0.0",
description: "Production ready example module for Medusa.",
dependencies: {
hard: "",
soft: "",
node: []
}
});
}
permissions() {
return [
{
id: "example:manage",
name: "Example ( Manage )",
description: "Administrative access to configure and manage example items.",
group: "Example",
module: "example"
}
];
}
async onStartup() {
this.medusa.console.log(this.medusa.console.levels.startup, `${this.getName()} module initialized successfully.`);
}
async onShutdown() {
this.medusa.console.log(this.medusa.console.levels.info, `${this.getName()} module deloaded.`);
}
}Module Metadata Properties
The configuration object passed to super(medusa, meta) accepts the following properties:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique module identifier. Must match directory name in lowercase. |
displayName | string | No | Display title shown on the web dashboard (defaults to name). |
icon | string | No | Lucide icon identifier shown across dashboard module, configuration, and logging lists (defaults to "puzzle"). |
author | string | Yes | Name of the module author or organization. |
version | string | Yes | Semantic version string (e.g. 1.0.0.0). |
description | string | Yes | Short explanation of module capabilities. |
dependencies | object | No | Dependency map containing hard, soft, and node dependencies. node is a string[] of npm package names the module imports. They are installed automatically when the module registers, before any of its handlers load, so they never go in the root package.json. |
formerly | string[] | No | Previous module ids. On startup, stored config, language overrides, log settings, per-server toggles and rank permissions saved under an old id move to the current id, and keys inside them that start with the old id are renamed to its camelCase form. |
Renaming a Module
Rename the folder and name, then list the old id in formerly so existing servers keep their settings:
super(medusa, {
name: "join-to-create",
formerly: ["voicehub"],
displayName: "Join to Create"
});Rename config groups and log categories that started with the old id to the camelCase form of the new id (voicehub becomes joinToCreate, voicehubEvents becomes joinToCreateEvents) so the migrated values line up. Give renamed models a formerly table name too.
If the old folder is still on disk, the loader skips it with a warning so both ids never load together. For official modules, the release build records every rename in package.json under formerly, and the auto-updater installs the renamed module into its new folder and deletes the old one. A folder listed in MODULE_FROZEN is never deleted.
Module Methods
The Modules class provides the following built-in methods:
| Method | Return Type | Description |
|---|---|---|
getName() | string | Returns the module display name or identifier. |
getIcon() | string | Returns the module icon name. |
getAuthor() | string | Returns the module author string. |
getVersion() | string | Returns the module version string. |
getFormerly() | string[] | Returns the previous module ids declared in formerly. |
getDescription() | string | Returns the module description. |
getDependencies() | object | Returns the dependency configuration object. |
permissions() | Array<object> | Returns custom interaction permission nodes available for assignment in the dashboard. |
onStartup() | Promise<void> | Invoked after all module subsystems, database models, routes, and commands have loaded. |
onShutdown() | Promise<void> | Invoked during bot shutdown or module unloading. |
Library Exports: #medusa/modules
Modules import all required base classes from the #medusa/modules import alias:
import {
Modules,
Commands,
Events,
Clocks,
Models,
Routes,
Handlers,
Pages,
Configs,
Langs,
Logs,
Leaderboards,
Posts,
Nodes
} from "#medusa/modules";Cross-Module Communication
Every subsystem receives the root medusa instance. The this.medusa.modules manager allows any module to access any registered subsystem across the entire bot:
const economyHandler = this.medusa.modules.handlers.get("economy", "balances");
const ticketsModel = this.medusa.modules.models.get("ticket", "ticket_tickets");
const moderationRoute = this.medusa.modules.routes.get("moderation", "cases");
const coreCommand = this.medusa.modules.commands.get("core", "ping");
const configuredCurrency = this.medusa.modules.config.value("economy", "settings", "currencySymbol");
const localizedWelcome = this.medusa.modules.lang.phrase("core", "messages", "welcome", { user: interaction.user.id });Module Registry Accessors
| Accessor | Signature | Description |
|---|---|---|
this.medusa.modules.models.get | (module, name) => Models | Retrieves a database model instance. |
this.medusa.modules.handlers.get | (module, name) => Handlers | Retrieves a business logic handler instance. |
this.medusa.modules.routes.get | (module, name) => Routes | Retrieves an HTTP route instance. |
this.medusa.modules.commands.get | (module, name) => Commands | Retrieves a registered Discord command instance. |
this.medusa.modules.commands.find | (name) => Commands | Searches all loaded modules for a command by name. |
this.medusa.modules.events.get | (module, name) => Events | Retrieves a Discord gateway event instance. |
this.medusa.modules.clocks.get | (module, name) => Clocks | Retrieves a scheduled clock instance. |
this.medusa.modules.config.get | (module, name) => Configs | Retrieves a configuration group instance. |
this.medusa.modules.config.value | (module, name, key) => any | Reads a specific configuration setting value. |
this.medusa.modules.config.values | (module, name) => object | Reads all key-value pairs for a configuration group. |
this.medusa.modules.lang.get | (module, name) => Langs | Retrieves a language group instance. |
this.medusa.modules.lang.phrase | (module, name, key, tokens) => string | Resolves a localized phrase with token replacement. |
this.medusa.modules.lang.embed | (module, name, key, tokens) => EmbedBuilder | Resolves a localized embed template with tokens. |
this.medusa.modules.logs.get | (module, name) => Logs | Retrieves a logging category instance. |
this.medusa.modules.pages.get | (module, name) => Pages | Retrieves a custom dashboard page instance. |
this.medusa.modules.pages.list | () => Array<object> | Returns all registered custom dashboard pages. |
this.medusa.modules.leaderboards.get | (module, name) => Leaderboards | Retrieves a leaderboard provider instance. |
this.medusa.modules.leaderboards.all | () => Array<object> | Returns all registered leaderboard providers. |
this.medusa.modules.posts.get | (module, name) => Posts | Retrieves a post provider instance. |
this.medusa.modules.posts.all | (guildId) => Array<object> | Returns every post provider from modules enabled in that server. |
this.medusa.modules.placeholders.get | (module, name) => Placeholders | Retrieves a placeholder provider instance. |
this.medusa.modules.placeholders.all | (guildId) => Array<object> | Returns every placeholder provider from enabled modules. |
this.medusa.modules.nodes.get | (module, name) => Nodes | Retrieves an automation node instance. |
this.medusa.modules.nodes.all | () => Array<object> | Returns all registered automation nodes. |