Plexcord LogoPlexcord
Core Concepts

Plugin Lifecycle

Understand the complete Plexcord plugin lifecycle. Learn when start(), stop(), and startAt run, and how to manage resources correctly.

Plugin Lifecycle

Understanding when your plugin code runs is critical for writing correct, well-behaved plugins. This guide covers the complete lifecycle from Discord startup to plugin shutdown.

Overview

Discord launches
    └── Plexcord initializes
          ├── StartAt.Init        → plugins with startAt: StartAt.Init
          ├── StartAt.WebpackReady → plugins with startAt: StartAt.WebpackReady
          └── StartAt.DOMContentLoaded (default) → most plugins
                └── start() is called
                       └── [plugin runs until disabled or Discord closes]
                              └── stop() is called

Phase 1: Discord Starts

When Discord launches, the Plexcord core loader runs before most of Discord's UI is available. At this point:

  • The DOM may not be fully loaded
  • Discord Webpack modules may not be available
  • Most Discord APIs are not accessible

Phase 2: Choosing Your Init Point with startAt

The startAt property controls exactly when your plugin's start() function is called during the Discord startup sequence.

import definePlugin, { StartAt } from "@utils/types";

export default definePlugin({
    name: "MyPlugin",
    description: "...",
    authors: [{ name: "You", id: 0n }],

    startAt: StartAt.WebpackReady,  // Start after Webpack modules load

    start() {
        // Webpack modules are guaranteed available here
        const { MessageStore } = require("@webpack/common");
    }
});

StartAt Values

ValueWhen it runsAvailable APIs
StartAt.InitVery early, before Discord rendersMinimal: only native APIs
StartAt.WebpackReadyAfter Webpack module system loadsDiscord modules via @webpack
StartAt.DOMContentLoadedAfter DOM is interactive (default)Full DOM + Discord APIs

When in doubt, use the default. StartAt.DOMContentLoaded is appropriate for 95% of plugins. Only use Init or WebpackReady if you need to run code before the UI is ready.

Phase 3: Plugin Initialization with start()

start() is called once when your plugin is activated. This happens at:

  • Discord startup (if plugin is already enabled)
  • When the user enables the plugin in settings
  • Plugin reload during development
start() {
    // ✅ Set up intervals
    this.interval = setInterval(() => this.tick(), 5000);

    // ✅ Add DOM event listeners
    document.addEventListener("keydown", this.onKeyDown = this.onKeyDown.bind(this));

    // ✅ Subscribe to Flux events (done via the flux: {} property, not here)

    // ✅ Modify the DOM
    document.body.classList.add("my-plugin-active");

    console.log("[MyPlugin] Started");
},

What to Do in start()

  • Initialize timers and intervals
  • Register event listeners
  • Mutate the DOM if needed
  • Load plugin state from storage
  • Set up your plugin's core logic

Phase 4: Plugin Runs

While enabled, your plugin reacts to events via:

  • Flux listeners: Discord internal events (MESSAGE_CREATE, etc.)
  • Patches: Code modifications active while plugin is enabled
  • Commands: Slash commands registered while plugin is enabled
  • DOM observers: Used when you set up mutation observers

Phase 5: Plugin Shutdown with stop()

stop() is called when:

  • The user disables the plugin in settings
  • Discord is closing
  • Plugin is being reloaded during development

This is the most important phase to get right. Every resource created in start() must be cleaned up here. If you don't, effects will linger after the plugin is disabled.

stop() {
    // ✅ Clear timers
    clearInterval(this.interval);
    clearTimeout(this.timeout);

    // ✅ Remove DOM event listeners
    document.removeEventListener("keydown", this.onKeyDown);

    // ✅ Undo DOM changes
    document.body.classList.remove("my-plugin-active");

    // ✅ Reset state
    this.cache = null;

    console.log("[MyPlugin] Stopped");
},

The Golden Rule of stop()

Remove everything you added in start().

Failing to clean up leads to:

  • Visual artifacts in Discord's UI
  • Memory leaks
  • Event handlers firing after plugin is disabled
  • Confusing bugs that appear intermittently

Storing Plugin State

Use this to store state within your plugin instance:

export default definePlugin({
    name: "MyPlugin",
    description: "...",
    authors: [{ name: "You", id: 0n }],

    // Declare properties for TypeScript
    interval: null as ReturnType<typeof setInterval> | null,
    messageCount: 0,

    start() {
        this.messageCount = 0;
        this.interval = setInterval(() => {
            console.log(`Messages since start: ${this.messageCount}`);
        }, 10000);
    },

    stop() {
        if (this.interval) clearInterval(this.interval);
    },

    flux: {
        MESSAGE_CREATE() {
            // Access via Vencord.Plugins.plugins.MyPlugin.messageCount
            // Note: 'this' in flux handlers refers to the plugin object
        }
    }
});

Async Initialization

If your start() needs to do async work (fetch data, load from storage), use an async IIFE:

start() {
    // Don't make start() async: use an IIFE instead
    void (async () => {
        const data = await DataStore.get("myPlugin_data");
        this.cachedData = data ?? [];
        console.log("[MyPlugin] Data loaded:", this.cachedData.length, "items");
    })();
},

Don't make start() itself async. Plexcord doesn't await the return value. Instead, use a self-calling async function inside start().

Complete Lifecycle Example

import definePlugin from "@utils/types";
import { Logger } from "@utils/Logger";

const logger = new Logger("LifecycleDemo", "#00BFFF");

export default definePlugin({
    name: "LifecycleDemo",
    description: "Demonstrates the full plugin lifecycle",
    authors: [{ name: "Dev", id: 0n }],

    // State
    ticker: null as ReturnType<typeof setInterval> | null,
    count: 0,

    start() {
        logger.info("Plugin starting...");

        this.count = 0;
        this.ticker = setInterval(() => {
            this.count++;
            logger.info(`Tick #${this.count}`);
        }, 3000);

        document.body.dataset.lifecycleDemo = "active";
        logger.info("Plugin started. Ticker running every 3s.");
    },

    stop() {
        logger.info("Plugin stopping...");

        if (this.ticker) {
            clearInterval(this.ticker);
            this.ticker = null;
        }

        delete document.body.dataset.lifecycleDemo;
        logger.info(`Plugin stopped. Total ticks: ${this.count}`);
    }
});

Next Steps

On this page