EmDash Guides

Building Your First EmDash Plugin

A step-by-step guide to creating, testing, and publishing your first plugin on the EmDash marketplace.

EmDash GuidesApril 28, 20262 min read

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 capabilities array is exhaustive — email:send is the only external action this plugin can take
  • ctx only exposes bindings for declared capabilities — ctx.database would be undefined here
  • 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 entries
  • write:content — create or update entries
  • delete:content — delete entries
  • email:send — send transactional emails
  • storage:read / storage:write — access the media library
  • http:fetch:{hostname} — make outbound HTTP requests to a specific host
  • kv: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 update
  • content:beforeDelete / content:afterDelete
  • media:afterUpload — fires after a file is uploaded to the media library
  • admin:dashboard — render a custom widget in the admin dashboard
  • site: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

  1. Create an account on emdash.market and complete Stripe Connect onboarding to receive payouts
  2. Bundle your plugin: npm pack — this creates a .tgz archive
  3. Go to Upload, select Plugin, fill in the name, tagline, description, and price
  4. Upload the .tgz archive along with a cover image
  5. 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.

Building Your First EmDash Plugin · emdash.market