Skip to main content
Settings are saved automatically.
Contents

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

  1. Overview
  2. Module Structure
  3. Module Manifest
  4. Lifecycle Hooks
  5. The ModuleAPI Object
  6. Slash Commands
  7. Discord Event Handlers
  8. EventBus Subscriptions
  9. Event Visibility & Permissions
  10. Private Module Whitelisting
  11. Submission & Review Process
  12. 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

  1. Develop and test your module locally.
  2. Sign in to the Pluto dashboard and navigate to Submit Module.
  3. Fill in your module ID, version, source URL, and upload your module as a .zip file.
  4. Add any relevant notes for the reviewers.

Option B: Programmatic Deployment (Pluto SDK)

  1. Navigate to the Developer Portal in the Pluto web interface.
  2. Generate a Developer API Key. Keep this secure; it will only be displayed once.
  3. In your local development environment, set up the Pluto SDK (pluto-sdk).
  4. Add your API Key to your .env file as PLUTO_API_KEY, along with PLUTO_API_URL and your TEST_GUILD_ID.
  5. Run the deployment script: ./bin/pluto-deploy.js /path/to/your/module
  6. 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