How EmDash plugins work

An EmDash plugin is a sandboxed package that extends the CMS by declaring every capability, hook, storage collection, and admin surface it uses in a manifest.json file. The runtime grants nothing the manifest does not list.

Who this is for: plugin authors, site operators reviewing a plugin before installing, and anyone curious about how the isolation model actually works.

The contract

EmDash plugins are manifest-first. Before the CMS loads a plugin, it validates its manifest.json against a strict schema. The manifest becomes the plugin’s public contract:

  • Identity — id (lowercase name or @scope/name) and version (strict semver).
  • Capabilities — the operations the plugin is allowed to perform.
  • Storage — named document collections with their indexes.
  • Hooks — lifecycle events the plugin attaches to.
  • Admin surfaces — settings UI, dashboard pages, widgets, field editors.

If the manifest is invalid or the capabilities listed cannot be granted, the plugin does not load. Full reference: the manifest schema.

Capabilities and enforcement

EmDash defines a fixed vocabulary of 11 capabilities covering network fetch, content reads and writes, media reads and writes, user reads, email sending and interception, and page injection. A plugin must list each capability it uses in manifest.capabilities[].

Enforcement happens at the runtime boundary, not only at install time. A plugin that calls a method whose capability it didn’t declare gets an error, not a silent succeed. There is no default-allow path.

Full list: the capabilities reference.

Hooks

Plugins attach to named lifecycle hooks the CMS exposes. A hook can be listed as a plain string or as an object with an optional exclusivity, priority, and timeout:

"hooks": [ "content:beforeSave", { "name": "cron", "priority": 10, "timeout": 30000 } ]

Current hook names cover plugin lifecycle (plugin:install, plugin:activate, etc.), content events (content:beforeSave, content:afterSave), media events, email interception, comments, cron, and per-page metadata and fragment extension.

Storage

Storage is declarative: a plugin lists the document collections it needs and any indexes it relies on. The CMS provisions the collection and enforces the schema. A plugin cannot read or write a collection it did not declare.

"storage": { "redirects": { "indexes": ["from"], "uniqueIndexes": ["id"] } }

The install flow

There is no emdash CLI for plugin installs. The install flow goes entirely through the CMS admin dashboard:

  1. Site admin opens Plugins → Marketplace in EmDash admin.
  2. Picks a plugin, reviews the capabilities it requests, and clicks Install.
  3. EmDash core calls the marketplace API to download the bundle (/api/v1/plugins/:id/versions/:version/bundle).
  4. The bundle is copied into the site’s own R2 storage. The marketplace is a distribution channel, not a runtime dependency.
  5. On success, the CMS calls reportInstall, which POSTs a hashed site identifier to /api/v1/plugins/:id/installs — fire-and-forget, never throws.

Browser ZIP downloads count as downloads but not as installs — install counts on the marketplace only go up when a live EmDash site actually completes a successful install flow.

Also see: the manual install guide.

The audit

Every version uploaded to emdashcms.org passes through a fail-closed audit pipeline before it becomes downloadable:

  1. The bundle is unpacked inside an isolated worker sandbox.
  2. A static scanner checks the code against a public ruleset (no eval, no new Function, no child_process, and so on).
  3. An AI reviewer reads the code against the declared manifest, looking for capability drift and obvious risks.
  4. If the audit cannot reach a clean verdict, the version is rejected. Never auto-passed.

The verdict is visible on every plugin page and the full ruleset lives in the security policy.

Updated . Previous: ← what is EmDash CMS? · Next: the manifest schema →