Medusa/developer documentation/Hooks

Hooks

Change how Medusa behaves from a module without touching the framework.

The framework never checks for a specific module. When a module needs to change core behavior, it registers a hook on this.medusa.hooks. Remove the module and its hooks go with it.

modules/example/main.js
async onStartup() {
    this.medusa.hooks.register("restrict", this.restrict);
}

async onShutdown() {
    this.medusa.hooks.unregister("restrict", this.restrict);
}

Register in onStartup() and unregister in onShutdown() with the same function, so turning the module off removes it.

Registry

MethodWhat it does
register(name, handle)Adds a handle under name.
unregister(name, handle)Removes it.
has(name)Whether any handle is registered.
get(name, ...args)Calls each handle in order and returns the first result that is not undefined.
value(name, ...args)The same as get, without waiting. Handles must return plain values.
run(name, ...args)Calls every handle and ignores the results.
restricted(action, context)Asks the restrict hooks about action. Returns the reason as a string, or "" when allowed.
assert(action, context)Throws the reason when action is restricted.

Hooks Medusa Calls

HookCallArgumentsReturn
sessionget(request)A user for visitors with no session. Medusa marks it guest: true.
permissionsget(discordId)Permission list for that user. Skips the server role lookup.
signupget(discordId)Runs when a new dashboard user signs in.
serversget(discordId)Server list shown in the server picker.
scanrun(value, label)Gets every value saved from the dashboard or the API, such as config, phrases, presence and the bot username.
restrictrestricted(action, context)A reason string to block the action, or undefined to allow it.
modeget({ session }){ badge, hint, link, server }. Shows a badge next to the brand. badge and hint are lang keys, link and server are { label, href } or null.

Restrictions

Each action below is passed to restricted() before it runs.

ActionCovers
action:<name>A dashboard action, such as action:saveSettings.
page:<module>:<name>Hides a module page from the sidebar.
loginThe login page.
logsThe Logs page and its API.
ownerOwner-only settings.
modules, tokens, export, settingsLocks those dashboard sections.
writeEvery dashboard write through the API proxy. Context: { session, discordId, guild }.
automationSaving automation flows.
appeals.serverChoosing the appeals server.
servers.primaryChanging the primary server.

The shell receives restricted, the subset of logs, owner, modules, tokens, export and settings that is blocked, so the dashboard can hide those controls.

Demo Module

Every product ships its demo as the demo module on the demo/module branch. Customer releases leave it out. The demo build adds it and sets DEMO=true.

The demo module:

  • Sets hidden: true in its manifest so it never shows on the Modules page.
  • Registers its hooks in onStartup() only when process.env.DEMO === "true", and unregisters them in onShutdown().
  • Lists blocked actions in actions and blocked restrictions in denied. Anything not listed is allowed.
  • Returns its messages.locked phrase as the reason, and keeps its badge text in resources/lang/<code>/common.json.

It also adds trial servers: mode.server links to /m/demo/trial, and mode.link points to the public bot invite.

modules/demo/src/handlers/policy.js
restrict(action) {
    if (this.denied.includes(action)) return this.medusa.modules.lang.phrase("demo", "messages", "locked");
    if (action.startsWith("action:") && this.actions.includes(action.slice(7))) return this.medusa.modules.lang.phrase("demo", "messages", "locked");
    return undefined;
}