Introduction to the plugin system for developers.

Plugin SDK — Overview

This section is for developers. If you're not a programmer, you can skip this — everything in the previous sections works without writing any code.


The plugin system lets developers extend ED5 with custom game mechanics, chat commands, server events, HUD widgets, and client-side features. Plugins run in two places:

  • Server plugins — run on the game server with access to all game systems
  • Client plugins — run in the player's browser with HUD widget and chat APIs

Two Plugin Tiers

ED5 ships two plugin runtimes. They are not the same API — the sandboxed runtime exposes a curated ed5. namespace gated by capabilities, while trusted file plugins get a broad ctx. context. Know which one you're writing for.

Tier 1 — Sandboxed (the default)Tier 0 — Trusted (file-based)
Where it runsPer-project, including hosted/multi-tenantLocal / desktop Studio, first-party only
Server APIed5. (capability-gated)ctx. (full context)
PackagingDB-backed definition with a manifest + capabilities.ts/.js files auto-loaded from disk
IsolationQuickJS-WASM isolate — a 50 ms wall-clock budget per call and a 16 MB memory limit; auto-quarantined after repeated failuresFull trust (runs in-process)
Authored inThe /plugins studioThe /plugins studio or the editor's Plugins modal
DefaultOn by default — flags sandboxedPlugins / sandboxedClientPluginsAvailable locally

On hosted / multi-tenant projects, Tier 0 file plugins are disabled — everything runs sandboxed. So unless you're building first-party plugins for a local/desktop server, you're writing sandboxed ed5. plugins. The following pages document the trusted ctx. API in depth; the sandboxed ed5.* surface is summarized below.

The Plugins Studio (sandboxed)

The full-page /plugins studio is the authoring surface for sandboxed plugins:

  • AI codegen — describe a plugin and generate a first draft
  • CodeMirror editor with autocomplete against the real ed5.* API
  • Manifest & capabilities — see below
  • Dry-run — execute against a simulated context (no network / filesystem / DB) before saving
  • Publish — saves the definition. A published plugin starts disabled — you then explicitly enable it, and enabling is what grants its capabilities and activates it on the running server. Publishing alone does not roll it out.

The manifest

Every sandboxed plugin declares a manifest:

{
  name: string;        // /^[a-z][a-z0-9-]{1,48}$/
  version: string;
  runs: ("server" | "client" | "editor")[];   // at least one
  capabilities: string[];                      // from the catalog below
  summary?: string;    // human blurb, ≤ 400 chars  (note: NOT "description")
  author?: string;     // ≤ 120 chars
  aiGenerated?: boolean;
}

The sandboxed API — <code class="inline-code">ed5.*</code>

Sandboxed server code calls a curated namespace. Each method needs the capability shown (and nothing you don't request is reachable):

MethodCapability (risk)What it does
ed5.events.on(event, handler)events:read:&lt;event&gt; (R0)Run a handler when a game event fires (see Events Reference)
ed5.commands.register(name, handler)command:register:&lt;name&gt; (R1)Register a /slash command; handler gets { entityId, args, raw }
ed5.chat.send(entityId, message)chat:send (R1)Message one player
ed5.chat.broadcast(message)chat:broadcast (R1)Message everyone online
ed5.ui.hud(entityId, widget)ui:hud (R1)Show/update a HUD widget on a client
ed5.economy.grant(entityId, { gold?, itemId?, quantity? })economy:grant (R2)Give gold and/or an item
ed5.world.spawn({ mobDefinitionId, x, y, zoneId? })world:spawn (R2)Spawn a mob by definition
ed5.player.modify(entityId, { heal?, damage? })player:modify (R3)Change an entity's HP (clamped)
ed5.storage.get(key) / ed5.storage.set(key, value)storage:plugin (R0)This plugin's own key/value store
ed5.log(message)Write to the plugin log

There's also an instance:enter (R2) capability for sending a player into an instanced dungeon/arena. Capability strings are the single source of truth — one the catalog doesn't know can't be granted, so it can't be reached. Command names are validated by /^[a-z][a-z0-9_-]{0,31}$/i.

Your first sandboxed plugin

// manifest: { runs: ["server"], capabilities: ["events:read:player.login", "chat:send"] }
ed5.events.on("player.login", (e) => {
  ed5.chat.send(e.playerId, "Welcome to the server!");
});

> Risk tiers R0–R3 rank how much a capability can affect the game (R0 = read/own-storage, R3 = directly change a player). They drive review/approval, not a hard block.

Where Tier 0 Plugins Live

The editor's Plugins modal manages file-based Tier 0 plugins:

Plugin Manager — Plugin list with enable/disable toggles and Add New for server/client plugins
Plugin Manager — Plugin list with enable/disable toggles and Add New for server/client plugins
① Plugin list with enable/disable toggles② Built-in code editor with syntax highlighting③ Server and Client tabs for creating new plugins
You can also create Tier 0 plugin files directly on disk. The folder decides whether a file is a server or client plugin — not its extension. Both folders are read from the game-server package root and from your project data directory (ED5_PROJECT_DATA_DIR).
TypeFolderExtensions loaded
Server pluginsplugins/.ts and .js
Client pluginsclient-plugins/.js only

So a .js file in plugins/ is a server plugin, not a client widget.

Auto-Loading (Tier 0)

File-based plugins load automatically when the server starts. Just create a file (or use the built-in editor) and restart.

Naming Rules

PatternBehavior
plugins/my-plugin.ts (or .js)Server plugin — loaded automatically
client-plugins/my-widget.jsClient plugin — sent to all connected players
_disabled-plugin.tsSkipped (underscore prefix disables), in either folder

Your First Server Plugin (trusted / file-based)

The rest of this section documents the trusted ctx. API used by local file plugins. (Sandboxed plugins use ed5., shown above.) Create a server plugin as a .ts file in plugins/ (or in the editor):

import type { PluginDefinition } from "../src/core/plugin-system.js";

const plugin: PluginDefinition = { name: "welcome", version: "1.0.0", description: "Welcomes players on login",

register(ctx) { ctx.log.info("Welcome plugin loaded!");

ctx.eventBus.on("player:login", (e) => { ctx.sendSystemMessage(e.playerId, "Welcome to the server!"); });

ctx.registerCommand("hello", (entityId, args) => { const name = args[0] ?? "adventurer"; ctx.sendSystemMessage(entityId, Hello, ${name}!); }, { description: "Greet someone", usage: "/hello [name]", }); }, };

export default plugin;

Your First Client Plugin

Create a new client plugin in the Plugin editor (or as a .js file):

ctx.log.info("Stats HUD loaded!");

var widgetId = ctx.createWidget({ html: buildHtml(ctx.getState()), anchor: "top-right", position: { x: 16, y: 80 }, width: 180, draggable: true, css: "background:rgba(0,0,0,0.75);" + "color:#eee;border:1px solid #555;" + "border-radius:8px;padding:10px;" + "font-family:monospace;font-size:12px;", });

ctx.onStateChange(function() { ctx.updateWidget(widgetId, { html: buildHtml(ctx.getState()), }); });

function buildHtml(state) { return "<div>" + "HP: " + state.selfHp + "/" + state.selfMaxHp + "<br>MP: " + state.selfMp + "/" + state.selfMaxMp + "<br>Gold: " + state.selfSilver + "</div>"; }

Plugin Structure

Every server plugin exports a PluginDefinition object:

interface PluginDefinition {
  name: string;          // Unique identifier
  version: string;       // e.g., "1.0.0"
  author?: string;
  description?: string;

register(ctx: PluginContext): void | Promise<void>; unregister?(): void | Promise<void>; }

The register function receives a PluginContext — this is the full API for interacting with the game server.

Auto-Cleanup

All subscriptions, commands, timers, and channels registered through ctx are automatically cleaned up when:

  • The server shuts down
  • The plugin is disabled or reloaded
The unregister() hook is optional and only needed for external resources.