Plugin System Overview
Plugin system overview
Section titled “Plugin system overview”This page is the 5-minute orientation. For the canonical end-to-end guide (manifest, permissions, extension-point cookbook, testing, signing, publishing), read Plugin Author Guide.
Plugins are the primary extension mechanism in ecommus. They register routes, subscribe to domain events, modify computed values via filters, inject admin UI, contribute payment / shipping drivers, niche presets, dashboard widgets, and migrations — without touching the core codebase.
How plugins are discovered
Section titled “How plugins are discovered”At startup, the SDK walks the plugins/ directory at the repo root.
Each subdirectory containing a package.json with an "ecommus" block
gets loaded in dependency order (topological sort).
plugins/├── example-hello/│ ├── package.json ← must contain ecommus.kind === "plugin"│ └── src/│ └── index.ts ← default export = PluginInit function└── niche-booking/ ├── package.json └── src/ └── index.tsThe manifest
Section titled “The manifest”The manifest lives in package.json under the "ecommus" key:
{ "name": "@ecommus-plugin/example", "version": "1.0.0", "ecommus": { "kind": "plugin", "minCoreVersion": "0.3.0", "permissions": ["events.subscribe", "routes.register"], "dependencies": [], "entrypoint": "./src/index.ts" }}| Field | Required | Purpose |
|---|---|---|
ecommus.kind | yes | Must be "plugin". |
ecommus.minCoreVersion | yes | Lowest @ecommus/core version your plugin works against. |
ecommus.permissions | effectively yes | What the SDK lets your plugin do — see below. |
ecommus.dependencies | no | Other plugin names that must load first. |
ecommus.entrypoint | no | Default src/index.ts. |
ecommus.settingsSchema | no | If present, admin UI auto-renders a settings form. |
Permissions (P1 — enforced at registration time)
Section titled “Permissions (P1 — enforced at registration time)”As of session 33 (P1), permissions are load-bearing. Every
register* method on the PluginContext requires a matching
permission to be declared in the manifest. Strict mode (production)
throws PluginPermissionError on missing declarations; warn mode
(default in dev) logs a warning + allows the call.
| Context method | Required permission |
|---|---|
registerPaymentDriver | payments.register |
registerShippingProvider | shipping.register |
registerProductType | products.write |
registerPricingStrategy | products.write |
registerHook | events.subscribe |
registerFilter | events.filter |
registerRoute | routes.register |
registerAdminRoute | admin.routes.register |
registerAdminSlot | admin.ui.inject |
registerSection | cms.write |
registerMigration | migrations.run |
registerSeeder | migrations.run |
registerDashboardWidget | dashboard.widget.register |
registerNichePreset | dashboard.preset.register |
registerPluginMeta | (none — metadata only) |
The host’s enforcement mode is controlled by
ECOMMUS_PERMISSION_ENFORCEMENT=strict|warn|off.
The init function
Section titled “The init function”Every plugin exports a default PluginInit function:
import type { PluginInit } from "@ecommus/plugin-sdk";
const init: PluginInit = async (ctx) => { ctx.registerPluginMeta({ id: "@ecommus-plugin/example", label: "Example", category: "feature", icon: "Sparkles", });
ctx.registerHook("order.placed", async (event) => { ctx.logger.info({ orderId: event.payload.orderId }, "order observed"); });};
export default init;Init runs once per host process boot. It receives a PluginContext
scoped to your plugin (everything you register is tagged with your
plugin name).
The PluginContext
Section titled “The PluginContext”interface PluginContext { // identity name: string; version: string; config: Record<string, unknown>; // settings the operator filled in
// shared resources logger: PluginLogger; // pino-style scoped logger db?: PluginDb; // host-scoped DB handle (absent in tests)
// registration methods (each requires the permission listed above) registerPaymentDriver(code, factory): void; registerShippingProvider(code, factory): void; registerProductType(def): void; registerPricingStrategy(def): void; registerHook(event, handler): void; registerFilter<T>(name, handler): void; registerRoute(reg): void; registerAdminRoute(reg): void; registerAdminSlot(reg): void; registerSection(reg): void; registerMigration(reg): void; registerSeeder(reg): void; registerDashboardWidget(def): void; registerNichePreset(def): void; registerPluginMeta(meta): void;}Use ctx.logger, not console.log. The logger is structured
(Pino), goes to the host’s log pipeline, scoped to your plugin name.
Extension point quick examples
Section titled “Extension point quick examples”Hook (subscribe to a domain event)
Section titled “Hook (subscribe to a domain event)”ctx.registerHook("order.paid", async (event) => { await sendInvoiceEmail(event.payload.orderId);});Permission: events.subscribe. See Hooks & Filters for the event catalog.
Filter (modify a computed value)
Section titled “Filter (modify a computed value)”ctx.registerFilter<number>("product.price", async (price, context) => { if (context.product.tags.includes("vip-only")) return price * 0.8; return price;});Permission: events.filter.
Route (Fastify route on the storefront / API)
Section titled “Route (Fastify route on the storefront / API)”ctx.registerRoute({ scope: "storefront", prefix: "/my-plugin", register: (app) => { app.get("/health", async () => ({ ok: true })); },});Permission: routes.register.
Admin UI slot (inject a React component)
Section titled “Admin UI slot (inject a React component)”ctx.registerAdminSlot({ slot: "product.editor.tabs", componentPath: "@ecommus-plugin/my-feature/ui/EditorTab", priority: 10,});Permission: admin.ui.inject.
Plugin settings
Section titled “Plugin settings”If your manifest declares a settingsSchema, the admin UI auto-renders
a form at /admin/integrations/<plugin-name>/settings. The values land
on ctx.config at the next init. See Plugin Settings
for the schema format.
Where to next
Section titled “Where to next”- Plugin Author Guide — full guide (manifest, permissions, all 15 register* methods, testing, signing, publishing, best practices, anti-patterns)
- Creating a Plugin — step-by-step first plugin
- Hooks & Filters — event catalog
- Plugin Settings —
settingsSchemareference - Testing Plugins — Vitest + registry harness
If you want a worked example, the in-tree plugins/example-hello/
shows the minimum (route + hook + admin slot). plugins/niche-booking/
shows the full niche-package shape (migrations + dashboard widgets +
niche preset + multiple routes).