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 runs | Per-project, including hosted/multi-tenant | Local / desktop Studio, first-party only |
| Server API | ed5. (capability-gated) | ctx. (full context) |
| Packaging | DB-backed definition with a manifest + capabilities | .ts/.js files auto-loaded from disk |
| Isolation | QuickJS-WASM isolate — a 50 ms wall-clock budget per call and a 16 MB memory limit; auto-quarantined after repeated failures | Full trust (runs in-process) |
| Authored in | The /plugins studio | The /plugins studio or the editor's Plugins modal |
| Default | On by default — flags sandboxedPlugins / sandboxedClientPlugins | Available 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):
| Method | Capability (risk) | What it does |
|---|---|---|
ed5.events.on(event, handler) | events:read:<event> (R0) | Run a handler when a game event fires (see Events Reference) |
ed5.commands.register(name, handler) | command:register:<name> (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:

ED5_PROJECT_DATA_DIR).
| Type | Folder | Extensions loaded |
|---|---|---|
| Server plugins | plugins/ | .ts and .js |
| Client plugins | client-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
| Pattern | Behavior |
|---|---|
plugins/my-plugin.ts (or .js) | Server plugin — loaded automatically |
client-plugins/my-widget.js | Client plugin — sent to all connected players |
_disabled-plugin.ts | Skipped (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
unregister() hook is optional and only needed for external resources.