Medusa/Overview

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:

  1. Registration Phase: Medusa verifies licensing, installs any missing dependencies.node packages, initializes schemas, loads configuration groups, language dictionaries, logging categories, leaderboard providers, post providers, automation flow nodes, database models, HTTP routes, and custom dashboard pages.
  2. 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 core moduleDisabled warning.
  • Skips its Discord and internal events when the payload resolves to that server (guildId, guild, or message.guildId).
  • Hides its dashboard pages, configs, logging groups, leaderboard metrics, and /help entries for that server, and its routes answer 404 to 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:

DirectoryLoaded File PatternBase ClassExport Name
main.jsRoot module entrypointModulesModule Class
src/models/Every *.js file directly in folderModelsModel Class
src/handlers/Every *.js file directly in folderHandlersHandler Class
src/commands/Every *.js file directly in folderCommandsCommand Class
src/events/Every *.js file directly in folderEventsEvent Class
src/clocks/Every *.js file directly in folderClocksClock Class
src/routes/Every route.js file at any nested pathRoutesRoute Class
resources/config/Every *.js file directly in folderConfigsConfig Class
resources/logs/Every *.js file directly in folderLogsLog Class
resources/lang/English classes in en/*.js, translations in translations.jsonLangsLang Class
resources/leaderboards/Every *.js file directly in folderLeaderboardsLeaderboard Class
resources/posts/Every *.js file directly in folderPostsPost Class
resources/placeholders/Every *.js file directly in folderPlaceholdersPlaceholder Class
resources/nodes/Every *.js file directly in folderNodesNode Class
resources/dashboard/pages/Every page.js file at any nested pathPagesPage Class
resources/emojis/Every image file (.png, .gif, etc.) and optional emojis.jsonImage / JSONEmojis

Base Module Class

Every module entrypoint (modules/<name>/main.js) must export a default or named class extending Modules imported from #medusa/modules.

modules/example/main.js
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:

PropertyTypeRequiredDescription
namestringYesUnique module identifier. Must match directory name in lowercase.
displayNamestringNoDisplay title shown on the web dashboard (defaults to name).
iconstringNoLucide icon identifier shown across dashboard module, configuration, and logging lists (defaults to "puzzle").
authorstringYesName of the module author or organization.
versionstringYesSemantic version string (e.g. 1.0.0.0).
descriptionstringYesShort explanation of module capabilities.
dependenciesobjectNoDependency 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.
formerlystring[]NoPrevious 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:

modules/join-to-create/main.js
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:

MethodReturn TypeDescription
getName()stringReturns the module display name or identifier.
getIcon()stringReturns the module icon name.
getAuthor()stringReturns the module author string.
getVersion()stringReturns the module version string.
getFormerly()string[]Returns the previous module ids declared in formerly.
getDescription()stringReturns the module description.
getDependencies()objectReturns 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

AccessorSignatureDescription
this.medusa.modules.models.get(module, name) => ModelsRetrieves a database model instance.
this.medusa.modules.handlers.get(module, name) => HandlersRetrieves a business logic handler instance.
this.medusa.modules.routes.get(module, name) => RoutesRetrieves an HTTP route instance.
this.medusa.modules.commands.get(module, name) => CommandsRetrieves a registered Discord command instance.
this.medusa.modules.commands.find(name) => CommandsSearches all loaded modules for a command by name.
this.medusa.modules.events.get(module, name) => EventsRetrieves a Discord gateway event instance.
this.medusa.modules.clocks.get(module, name) => ClocksRetrieves a scheduled clock instance.
this.medusa.modules.config.get(module, name) => ConfigsRetrieves a configuration group instance.
this.medusa.modules.config.value(module, name, key) => anyReads a specific configuration setting value.
this.medusa.modules.config.values(module, name) => objectReads all key-value pairs for a configuration group.
this.medusa.modules.lang.get(module, name) => LangsRetrieves a language group instance.
this.medusa.modules.lang.phrase(module, name, key, tokens) => stringResolves a localized phrase with token replacement.
this.medusa.modules.lang.embed(module, name, key, tokens) => EmbedBuilderResolves a localized embed template with tokens.
this.medusa.modules.logs.get(module, name) => LogsRetrieves a logging category instance.
this.medusa.modules.pages.get(module, name) => PagesRetrieves 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) => LeaderboardsRetrieves a leaderboard provider instance.
this.medusa.modules.leaderboards.all() => Array<object>Returns all registered leaderboard providers.
this.medusa.modules.posts.get(module, name) => PostsRetrieves 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) => PlaceholdersRetrieves 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) => NodesRetrieves an automation node instance.
this.medusa.modules.nodes.all() => Array<object>Returns all registered automation nodes.