Skip to content

Plugin System Overview

EmDash’s plugin system lets you extend the CMS without modifying core code. Plugins can hook into content lifecycle events, store their own data, expose settings to administrators, and add custom UI to the admin panel.

EmDash plugins are configuration transformers, not separate applications. They run in the same process as your Astro site and interact through well-defined interfaces.

Key principles:

  • Declarative — Hooks, storage, and routes are declared at definition time, not registered dynamically
  • Type-safe — Full TypeScript support with typed context objects
  • Sandboxing-ready — APIs designed for isolated execution on Cloudflare Workers
  • Capability-based — Plugins declare what they need; the runtime enforces access

Hook into events

Run code before or after content saves, media uploads, and plugin lifecycle events.

Store data

Persist plugin-specific data in indexed collections without writing database migrations.

Expose settings

Declare a settings schema and get an auto-generated admin UI for configuration.

Add admin pages

Create custom admin pages and dashboard widgets with React components.

Create API routes

Expose endpoints for your plugin’s admin UI or external integrations.

Make HTTP requests

Call external APIs with declared host restrictions for security.

Every plugin is created with definePlugin():

import { definePlugin } from "emdash";
export default definePlugin({
id: "my-plugin",
version: "1.0.0",
// What APIs the plugin needs access to
capabilities: ["read:content", "network:fetch"],
// Hosts the plugin can make HTTP requests to
allowedHosts: ["api.example.com"],
// Persistent storage collections
storage: {
entries: {
indexes: ["userId", "createdAt"],
},
},
// Event handlers
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
// REST API endpoints
routes: {
status: {
handler: async (ctx) => ({ ok: true }),
},
},
// Admin UI configuration
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
},
pages: [{ path: "/dashboard", label: "Dashboard" }],
widgets: [{ id: "status", size: "half" }],
},
});

Every hook and route handler receives a PluginContext object with access to:

PropertyDescriptionAvailability
ctx.storagePlugin’s document collectionsAlways (if declared)
ctx.kvKey-value store for settings and stateAlways
ctx.contentRead/write site contentWith read:content or write:content
ctx.mediaRead/write media filesWith read:media or write:media
ctx.httpHTTP client for external requestsWith network:fetch
ctx.logStructured loggerAlways
ctx.pluginPlugin metadata (id, version)Always

The context shape is identical across all hooks and routes. Capability-gated properties are only present when the plugin declares the required capability.

Capabilities determine what APIs are available in the plugin context:

CapabilityGrants Access To
read:contentctx.content.get(), ctx.content.list()
write:contentctx.content.create(), ctx.content.update(), ctx.content.delete()
read:mediactx.media.get(), ctx.media.list()
write:mediactx.media.getUploadUrl(), ctx.media.delete()
network:fetchctx.http.fetch()

Register plugins in your Astro configuration:

astro.config.mjs
import { defineConfig } from "astro/config";
import { emdash } from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
import auditLogPlugin from "@emdash-cms/plugin-audit-log";
export default defineConfig({
integrations: [
emdash({
plugins: [seoPlugin({ generateSitemap: true }), auditLogPlugin({ retentionDays: 90 })],
}),
],
});

Plugins are resolved at build time. Order matters for hooks with the same priority—earlier plugins in the array run first.

EmDash supports two plugin execution modes:

ModeDescriptionPlatform
TrustedPlugins run in-process with full accessAny
SandboxedPlugins run in isolated V8 workersCloudflare only

In trusted mode (the default), capabilities are documentation—plugins can access anything. In sandboxed mode, capabilities are enforced at the runtime level.