Developer API
Pluto Module Developer API
Pluto — The extensible Discord bot platform by Nebula Corporation. This document is the authoritative reference for third-party module developers.
Table of Contents
- Overview
- Module Structure
- Module Manifest
- Lifecycle Hooks
- The ModuleAPI Object
- Slash Commands
- Discord Event Handlers
- EventBus Subscriptions
- Event Visibility & Permissions
- Private Module Whitelisting
- Submission & Review Process
- Complete Example Module
1. Overview
Modules are ES Module packages installed in either:
./src/modules/<id>/— Built-in Nebula Corp modules../modules/<id>/— User-installed / third-party modules.
Each module directory must contain an index.js that export defaults a Module Definition Object matching the manifest described below.
Module Isolation
Each module receives its own ModuleAPI instance scoped to (moduleId, guildId). Your module code runs in the same Node.js process as Pluto, not a sandbox. Modules are trusted after Nebula Corp review. Malicious modules will be disabled and the submitting guild may be banned.
2. Module Structure
modules/
com.example.greeting/
index.js ← required: default-exports the Module Definition
package.json ← optional: metadata, dependencies
README.md ← optional: shown in the marketplace
3. Module Manifest
Your index.js default export must be a plain object:
export default {
// ── Required ───────────────────────────────────────────────
id: 'com.example.greeting', // unique reverse-domain ID
name: 'Greeting Module',
version: '1.0.0',
// ── Recommended ────────────────────────────────────────────
description: 'Sends a welcome message when new members join.',
author: 'Your Name',
// ── Visibility ─────────────────────────────────────────────
visibility: 'PUBLIC', // 'PUBLIC' | 'PRIVATE'
// PUBLIC → any guild may subscribe
// PRIVATE → only guilds whitelisted by the owning guild
// ── Internal flag (Nebula Corp use only) ───────────────────
// isInternal: false, // Only true for official Nebula modules.
// // Setting this to true in a submitted module
// // will be overridden during review.
// ── Subscription ───────────────────────────────────────────
subscriptionType: 'FREE', // 'FREE' | 'PAID'
pricePerMonth: 0, // Torn City points, if PAID
// ── Lifecycle hooks (see §4) ────────────────────────────────
async onLoad() { /* ... */ },
async onGuildEnable(guildId, api) { /* ... */ },
async onGuildDisable(guildId, api) { /* ... */ },
async onUnload() { /* ... */ },
// ── Slash commands (see §6) ────────────────────────────────
commands: [ /* ... */ ],
// ── Discord gateway event handlers (see §7) ────────────────
discordEvents: { /* ... */ },
// ── EventBus subscriptions (see §8) ────────────────────────
eventSubscriptions: [ /* ... */ ],
// ── Declared emitted events (documentation / permission) ───
emittedEvents: [
{ name: 'com.example.greeting.sent', visibility: 'PUBLIC' },
],
};
4. Lifecycle Hooks
| Hook | Called when | api available |
|---|---|---|
onLoad() |
Module code is first loaded into Pluto's memory | No (global scope only) |
onGuildEnable(guildId, api) |
A guild activates the module | ✓ |
onGuildDisable(guildId, api) |
A guild deactivates the module | ✓ |
onUnload() |
Module is removed from the system | No |
Important: onGuildEnable is called once per guild, each time Pluto starts or the guild re-enables the module. Use api.store for persistence — do not assume in-memory state survives a restart.
5. The ModuleAPI Object
api is injected into every guild-scoped hook and handler. It is always bound to a specific (moduleId, guildId) pair.
5.1 Identity
api.moduleId // string — this module's ID
api.guildId // string — the Discord guild ID
api.config // object — guild-specific config (see §5.6)
5.2 Discord Client
api.client // discord.js Client (read-only)
api.guild // discord.js Guild object for api.guildId (or null)
5.3 EventBus
// Subscribe to an event
const unsub = api.on('some.event.name', async (payload, meta) => {
// meta = { eventName, sourceModuleId, guildId, visibility }
});
// Subscribe once (auto-removed after first delivery)
api.once('some.event.name', handler);
// Emit an event
await api.emit('com.example.greeting.sent', { userId: '...' });
// Optionally specify visibility:
await api.emit('my.private.event', data, 'PRIVATE');
// Unsubscribe manually
unsub();
All subscriptions registered via api.on() are automatically removed when the module is disabled.
5.4 Key/Value Store
Persistent storage scoped to (moduleId, guildId). Values are JSON-serialised.
await api.store.get(key) // → any | null
await api.store.set(key, value) // → void (upsert)
await api.store.delete(key) // → void
await api.store.getAll() // → { [key]: value }
await api.store.clear() // → void (delete all for this guild)
5.5 Database Access
Direct SQL access for advanced use cases. Prefer api.store for simple data.
const [rows, fields] = await api.dbQuery(sql, params);
const row = await api.dbQueryOne(sql, params); // first row or null
⚠️ Security: Never interpolate user input into SQL strings. Always use parameterised queries (
?placeholders).
5.6 Config
Guild administrators can store per-module configuration via the dashboard.
const config = await api.getConfig(); // → plain object
await api.setConfig({ channelId: '...' }); // replaces the entire config
Config is also available synchronously (as of the last load) via api.config.
5.7 Logging
api.log('info', 'Module started', { guildId });
api.log('warn', 'Channel missing');
api.log('error', 'Query failed', { err: err.message });
Log entries are tagged with module and guild automatically. Use this instead of console.log so output is captured by Pluto's log rotation.
6. Slash Commands
Commands are registered to each guild when the module is enabled there.
import { SlashCommandBuilder } from 'discord.js';
commands: [
{
// data must be a SlashCommandBuilder instance (or equivalent JSON).
data: new SlashCommandBuilder()
.setName('greet')
.setDescription('Greet a user.')
.addUserOption(opt =>
opt.setName('user').setDescription('Who to greet').setRequired(true)
),
async execute(interaction, api) {
const target = interaction.options.getUser('user');
await interaction.reply(`Hello, ${target}!`);
await api.emit('com.example.greeting.sent', {
fromUserId: interaction.user.id,
toUserId: target.id,
});
},
},
],
7. Discord Event Handlers
Receive Discord gateway events scoped to your module's guild automatically.
discordEvents: {
async messageCreate(message, api) {
if (message.author.bot) return;
// message is a discord.js Message
},
async guildMemberAdd(member, api) {
// member is a discord.js GuildMember
},
async voiceStateUpdate(oldState, newState, api) {
// ...
},
},
All standard discord.js gateway events are supported. The api parameter is appended to the normal discord.js event arguments.
8. EventBus Subscriptions
Declare subscriptions in the manifest for automatic wiring:
eventSubscriptions: [
{
event: 'nebula.info.command.used',
handler: async (payload, meta, api) => {
// payload = { command, userId, guildId }
// meta = { eventName, sourceModuleId, guildId, visibility }
},
},
],
Or subscribe imperatively in onGuildEnable:
async onGuildEnable(guildId, api) {
api.on('nebula.info.command.used', async (payload) => {
await api.store.set('lastCommandUser', payload.userId);
});
}
9. Event Visibility & Permissions
Every EventBus event has a visibility level:
| Level | Who can subscribe |
|---|---|
INTERNAL |
Only Nebula Corp internal modules (isInternal: true) |
PUBLIC |
Any loaded module |
PRIVATE |
Only modules whitelisted by the emitting module |
When emitting:
await api.emit('my.event', payload); // default: PUBLIC
await api.emit('my.event', payload, 'PRIVATE'); // only whitelisted modules
Internal events (e.g. core.bot.ready, core.guild.join) are INTERNAL and emitted by the Pluto core. Third-party modules cannot subscribe to these — only official Nebula Corp modules can.
10. Private Module Whitelisting
If your module emits PRIVATE events and you want specific third-party modules to receive them, add them to the whitelist via the Admin API:
POST /api/whitelist/:sourceModuleId/:subscriberModuleId
Authorization: Bearer <ADMIN_API_KEY>
Or through the Nebula Corp admin dashboard → Modules → Whitelist.
11. Submission & Review Process
There are two ways to submit your module to the Pluto marketplace: via the Web Dashboard or programmatically using the Pluto SDK.
Option A: Web Dashboard Submission
- Develop and test your module locally.
- Sign in to the Pluto dashboard and navigate to Submit Module.
- Fill in your module ID, version, source URL, and upload your module as a
.zipfile. - Add any relevant notes for the reviewers.
Option B: Programmatic Deployment (Pluto SDK)
- Navigate to the Developer Portal in the Pluto web interface.
- Generate a Developer API Key. Keep this secure; it will only be displayed once.
- In your local development environment, set up the Pluto SDK (
pluto-sdk). - Add your API Key to your
.envfile asPLUTO_API_KEY, along withPLUTO_API_URLand yourTEST_GUILD_ID. - Run the deployment script:
./bin/pluto-deploy.js /path/to/your/module - The SDK will automatically package your module, upload it via the API, and output the automated check results and linked review ticket directly to your terminal.
The Review Process
Once submitted, Nebula Corporation staff will review your code for:
- Security (no data exfiltration, no privilege escalation)
- Policy compliance (no spam, no abuse, no circumvention of restrictions)
- Code quality (reasonable, maintainable)
On approval, the module appears in the public marketplace.
⚠️ Note: Nebula Corp may revoke your Developer API Key and globally disable all your modules at any time for severe policy violations.
12. Complete Example Module
// modules/com.example.greeting/index.js
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
export default {
id: 'com.example.greeting',
name: 'Greeting',
version: '1.0.0',
description: 'Sends a configurable welcome message to new members.',
author: 'Your Name',
visibility: 'PUBLIC',
subscriptionType: 'FREE',
async onGuildEnable(guildId, api) {
api.log('info', 'Greeting module enabled');
},
async onGuildDisable(guildId, api) {
api.log('info', 'Greeting module disabled');
},
commands: [
{
data: new SlashCommandBuilder()
.setName('setwelcome')
.setDescription('Set the welcome channel for new members.')
.addChannelOption(opt =>
opt.setName('channel').setDescription('The channel').setRequired(true)
),
async execute(interaction, api) {
const channel = interaction.options.getChannel('channel');
await api.store.set('welcomeChannelId', channel.id);
await interaction.reply({
content: `Welcome messages will be sent to ${channel}.`,
ephemeral: true,
});
},
},
],
discordEvents: {
async guildMemberAdd(member, api) {
const channelId = await api.store.get('welcomeChannelId');
if (!channelId) return;
const channel = member.guild.channels.cache.get(channelId);
if (!channel) return;
const embed = new EmbedBuilder()
.setColor(0x7c5cfc)
.setTitle(`Welcome to ${member.guild.name}!`)
.setDescription(`Hey <@${member.id}>, glad you're here!`)
.setThumbnail(member.displayAvatarURL())
.setTimestamp();
await channel.send({ embeds: [embed] });
await api.emit('com.example.greeting.sent', {
userId: member.id,
guildId: member.guild.id,
});
},
},
eventSubscriptions: [],
emittedEvents: [
{ name: 'com.example.greeting.sent', visibility: 'PUBLIC' },
],
};
For support, questions, or to report policy violations, use the Support form in the Pluto dashboard.
© Nebula Corporation 2025