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(lowercasenameor@scope/name) andversion(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:
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.
The install flow
There is no emdash
CLI for plugin installs. The install flow goes entirely through the CMS
admin dashboard:
- Site admin opens Plugins → Marketplace in EmDash admin.
- Picks a plugin, reviews the capabilities it requests, and clicks Install.
- EmDash core calls the marketplace API to download the bundle (
/api/v1/plugins/:id/versions/:version/bundle). - The bundle is copied into the site’s own R2 storage. The marketplace is a distribution channel, not a runtime dependency.
- 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:
- The bundle is unpacked inside an isolated worker sandbox.
- A static scanner checks the code against a public ruleset (no
eval, nonew Function, nochild_process, and so on). - An AI reviewer reads the code against the declared manifest, looking for capability drift and obvious risks.
- 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 →