Cloudflare’s EmDash CMS launched on April 1, 2026, and the first question from every WordPress developer was the same: How do I write a plugin for it? The answer is radically different from what you know. WordPress plugins get full access to your database, file system, and user sessions—one compromised plugin and your entire site is exposed. In 2025 alone, security researchers disclosed 11,334 new WordPress vulnerabilities, and 96% of all WordPress security issues trace back to plugins. EmDash fixes this architecturally: every plugin runs in its own V8 isolate via Cloudflare’s dynamic workers, with permissions declared upfront in a capability manifest. A plugin that declares read:contentcodecodecode and email:sendcodecodecode can literally do nothing else—no database queries, no file system access, no unrestricted network calls.
This tutorial walks you through writing your first EmDash plugin from scratch. You’ll build a “Notify Admin on New Post” plugin that sends an email when a post is published. By the end, you’ll understand the security model, the hook system, and how to deploy your plugin to Cloudflare’s runtime.
The sandbox model only works on Cloudflare's runtime.
What Makes EmDash Plugins Different?
The fundamental difference is isolation. In WordPress, a plugin is a PHP script that sits alongside the core code. It calls global wpdbcodecodecode and has unrestricted access to every table. In EmDash, plugins run in their own dynamic worker—a lightweight, hardware-isolated V8 isolate that spins up in milliseconds and disappears when done. The plugin cannot touch your database, your theme, or other plugins unless the capability manifest explicitly grants it.
Here’s what that looks like in practice. A typical WordPress contact form plugin can read, write, and delete anything. An EmDash plugin with the same feature declares exactly two capabilities: read:contentcodecodecode (to read existing pages) and email:sendcodecodecode (to send notifications). The runtime enforces these boundaries. If a compromised update tries to read password hashes or phone home to an external server, the isolate physically blocks it. This is not policy—it’s architectural enforcement. V8 isolates, Linux namespaces, seccomp filters, and hardware memory protection keys all work together.
The downside? Full sandboxing requires Cloudflare’s dynamic workers, which are only available on the Workers Paid plan ($5/month). On the free tier or when self-hosting on a Node.js server, plugins run in-process without isolation. EmDash is open source under MIT, but the headline security feature depends on Cloudflare’s runtime.
Prerequisites
Before you start, you need:
- Node.js 22.12 or later (EmDash uses Astro 6, which requires this minimum)
- An EmDash instance running locally (run
npm create md-latestcodecodecode and follow the setup wizard) - A Cloudflare account (for deploying with sandboxed plugin support; the free tier works for basic testing)
- An email service (for the plugin example, we’ll use a simple SMTP configuration)
If you haven’t installed EmDash yet, spin up a playground at https://emdashcms.com/playgroundcodecodecode—it generates a temporary working site that lasts an hour.
Step 1: Scaffold a Plugin Project
EmDash plugins are JavaScript modules that export a configuration object. There’s no CLI generator yet (this is v0.1.0), so we’ll create the files manually.
Create a new directory for your plugin:
“`bash
mkdir notify-admin-plugin
cd notify-admin-plugin
“`
Inside, create two files: index.jscodecodecode (the plugin logic) and manifest.jsoncodecodecode (the capability declaration). EmDash uses a definePlugincodecodecode function from the @emdash-cms/plugin-sdkcodecodecode package. Install it locally:
“`bash
npm init -y
npm install @emdash-cms/plugin-sdk
“`
Step 2: Define the Plugin Manifest and Capabilities
The manifest is the core of EmDash’s security model. It declares exactly what your plugin can access. Open manifest.jsoncodecodecode:
“`json
{
“id”: “notify-admin”,
“version”: “0.1.0”,
“capabilities”: [
“read:content”,
“email:send”
]
}
“`
Each capability maps to a specific permission. read:contentcodecodecode allows reading posts and pages (but not writing or deleting). email:sendcodecodecode allows sending emails through the configured SMTP provider. The plugin cannot access the database, the file system, or any external network unless you add more capabilities. A plugin that only needs to read content and send email—like this one—declares exactly those two things. No skeleton key.
Step 3: Write the Hook Logic
EmDash provides a hook system similar to WordPress actions. The most relevant hook for our plugin is onPublishcodecodecode, which fires when a post transitions from draft to published. Our plugin will listen for that hook, read the published post’s title and author, and send an email to the site admin.
Open index.jscodecodecode:
“`javascript
import { definePlugin } from ‘@emdash-cms/plugin-sdk’;
export default definePlugin({
id: ‘notify-admin’,
version: ‘0.1.0’,
capabilities: [‘read:content’, ’email:send’],
hooks: {
async onPublish({ post, context }) {
// Only proceed if the post is newly published
if (post.status !== ‘published’) return;
// Read the post content (we only have read:content capability)
const title = post.title;
const author = post.author?.displayName || ‘Unknown’;
// Use the email capability from the context
const email = context.email;
// Send notification to the admin
await email.send({
to: ‘[email protected]’,
subject: New post published: ${title}codecodecode,
body: A new post titled "${title}" was published by ${author}.codecodecode,
});
},
},
});
“`
Key details about this code:
- The
definePlugincodecodecode function validates that the declared capabilities match the actual capabilities used in the hooks. If our code tried to callcontext.db.query()codecodecode, the runtime would throw an error becausedbcodecodecode is not in the manifest. - The
contextcodecodecode object provides scoped access to the capabilities declared. Foremail:sendcodecodecode, you getcontext.email.send()codecodecode. Forread:contentcodecodecode, you getcontext.content.read()codecodecode. You never get raw database or network access. - Hooks run asynchronously. The
onPublishcodecodecode hook receives apostcodecodecode object with the published content. The dynamic worker spins up only when this hook fires, executes the function, and terminates. No persistent processes, no memory leaks.
Step 4: Test Locally
With your EmDash instance running locally (on localhost:4321codecodecode), copy your plugin directory into the plugins/codecodecode folder of your EmDash project. The exact path depends on your project structure—check the EmDash documentation for the current convention.
Then restart the development server. EmDash will detect the plugin and register it. Publish a test post and check the server logs. If the email send fails, you’ll see an error in the console. Note that local testing runs plugins in-process—the sandbox isolation only activates when deployed to Cloudflare’s runtime with dynamic workers.
To verify the plugin works end-to-end without actual email, you can add a console log:
“`javascript
await email.send({ … });
console.log(‘Email sent successfully’);
“`
If you see the log, the hook fired correctly.
Step 5: Deploy to Cloudflare with Sandboxed Plugins
To get the full sandbox protection, you need to deploy your EmDash site to Cloudflare using the Workers Paid plan ($5/month). The deployment process is:
- Push your EmDash project (including the plugin) to a GitHub repository.
- Go to the Cloudflare Dashboard, click Workers & Pages, then Create application.
- Choose EmDash from the templates (or connect your GitHub repo).
- Select the Paid plan tier when prompted—this enables dynamic workers.
- Click Create and deploy.
Cloudflare will provision the infrastructure: Workers for compute, D1 for the database, R2 for media storage, and KV for plugin state. Your plugin now runs inside a V8 isolate with full sandboxing. The capability manifest is enforced at the runtime level. If a future update to your plugin tries to access context.dbcodecodecode, the isolate will refuse—even if the code itself is buggy or malicious.
Best Practices for Plugin Security
The capability manifest is powerful, but it’s not a magic shield. Follow these rules when building EmDash plugins:
- Declare the minimum capabilities. If your plugin only needs to read content, don’t add
write:contentcodecodecode. If it doesn’t need external network, leave outnetwork:fetchcodecodecode. - Never request
admin:usercodecodecode ordb:*codecodecode unless absolutely required. These capabilities grant broad access. Most plugins don’t need them. - Pin your plugin’s version. Users should see exactly which version they’re installing. Use semantic versioning.
- Test with a malicious scenario. Before publishing, try to make your plugin do something outside its declared capabilities. The runtime should block it. If it doesn’t, file a bug with the EmDash team.
The Plugin Ecosystem Today
EmDash launched with zero third-party plugins. As of this writing, the marketplace is empty. But the community is already active: a Pantheon product manager submitted a PR to port the Gutenberg editor, and Yoast’s founder Joost de Valk has praised the architecture. Cloudflare’s strategy is to let AI agents generate plugins using the built-in MCP server and agent skills files. The MIT license removes the GPL friction that kept commercial developers away from WordPress.
Still, this is v0.1.0. Expect breaking changes. The hook API may evolve, the manifest format may gain new fields, and the dynamic worker pricing could shift. If you’re building plugins for production clients today, budget for maintenance.
Where This Falls Short
The sandbox model only works on Cloudflare’s runtime. Self-host EmDash on a regular Node.js server and plugins run in-process with no isolation. The feature that justifies EmDash’s existence requires a paid Cloudflare subscription. That’s a dependency on a single vendor—something WordPress avoids by design. WordPress runs on any server with PHP and MySQL; EmDash’s security model is architecturally locked to Cloudflare.
For a small blog with a static theme and no plugins, the free tier is fine. But the moment you need a dynamic worker for sandboxing, you’re on the paid plan. That $5/month is cheap, but it’s still a vendor lock-in that WordPress avoids.
The Bigger Picture
EmDash’s plugin architecture solves a problem that has plagued WordPress for 24 years: plugins with full access to everything. By enforcing capabilities at the runtime level, Cloudflare has built a system where one compromised plugin cannot take down your entire site. The median time from vulnerability disclosure to mass exploitation in WordPress is 5 hours. In EmDash, a buggy plugin can only do what its manifest allows—and nothing more.
That’s the real innovation here. Not TypeScript, not serverless, not Astro. The sandbox. If you’re building a greenfield content site and security matters, this architecture is worth a serious look. Just don’t expect a plugin marketplace tomorrow.
- What makes EmDash plugins different from WordPress plugins?EmDash plugins run in isolated V8 workers with declared capabilities, while WordPress plugins have full access to the database and file system.
- What are the prerequisites for writing an EmDash plugin?You need Node.js 22.12 or later, a local EmDash instance, a Cloudflare account for sandboxed deployment, and an email service for the example plugin.
- Does EmDash's security model work on self-hosted servers?No, the sandbox model only works on Cloudflare's runtime. Self-hosted EmDash runs plugins in-process without isolation.