Overview
EmDash plugins are TypeScript modules that hook into the CMS lifecycle. They declare capabilities (permissions) in a manifest and respond to hooks (events). A plugin with 50,000 lines of code that only declares read:content can literally do nothing else — no network access, no database writes, no filesystem operations.
Prerequisites
- A working EmDash site (see Getting Started)
- Node.js v22+ and npm
- Basic TypeScript knowledge
Step 1 — Scaffold the Plugin
In your EmDash project root, create a new directory for your plugin inside ./plugins/:
mkdir -p plugins/my-first-plugin
cd plugins/my-first-plugin
npm init -y
npm install emdash --save-peer
Step 2 — Write the Plugin
Create index.ts. Here is a complete plugin that sends an email notification whenever a post is published:
import { definePlugin } from "emdash";
export default () =>
definePlugin({
id: "notify-on-publish",
version: "1.0.0",
capabilities: ["read:content", "email:send"],
hooks: {
"content:afterSave": async (event, ctx) => {
// Only act on published posts
if (
event.collection !== "posts" ||
event.content.status !== "published"
) return;
await ctx.email!.send({
to: "editors@example.com",
subject: `New post: ${event.content.title}`,
text: `"${event.content.title}" is now live at /posts/${event.content.slug}`,
});
ctx.log.info(`Notified editors about post ${event.content.id}`);
},
},
});
Key things to notice:
- The
capabilitiesarray is exhaustive —email:sendis the only external action this plugin can take ctxonly exposes bindings for declared capabilities —ctx.databasewould beundefinedhere- The plugin is a factory function (the outer arrow), allowing dependency injection in tests
Step 3 — Available Capabilities
EmDash provides a growing set of capabilities you can declare:
read:content— read any collection's entrieswrite:content— create or update entriesdelete:content— delete entriesemail:send— send transactional emailsstorage:read/storage:write— access the media libraryhttp:fetch:{hostname}— make outbound HTTP requests to a specific hostkv:read/kv:write— read/write key-value storage for plugin state
Step 4 — Available Hooks
Hook your plugin into the CMS lifecycle:
content:beforeSave/content:afterSave— fires on create and updatecontent:beforeDelete/content:afterDeletemedia:afterUpload— fires after a file is uploaded to the media libraryadmin:dashboard— render a custom widget in the admin dashboardsite:request— intercept and respond to HTTP requests (useful for building API endpoints)
Step 5 — Test Locally
Because your plugin lives in ./plugins/my-first-plugin/, EmDash will load it automatically when you run npm run dev. Check the terminal for any sandbox errors, and use ctx.log.info() generously — logs appear in the admin panel under Plugins → Logs.
Step 6 — Publish to the Marketplace
- Create an account on emdash.market and complete Stripe Connect onboarding to receive payouts
- Bundle your plugin:
npm pack— this creates a.tgzarchive - Go to Upload, select Plugin, fill in the name, tagline, description, and price
- Upload the
.tgzarchive along with a cover image - Submit for review — our automated security audit runs within minutes and will either approve, flag, or reject the submission
Once approved, your plugin appears publicly on the marketplace and you earn 80% of every sale.
