EmDash — The Astro-Native CMS That's the Spiritual Successor to WordPress (Self-Hosted Guide)
EmDash — The Modern Astro-Native CMS: Complete Self-Hosting Guide
What if WordPress were rebuilt from scratch with today's tools — Astro, TypeScript, sandboxed plugins, and structured content? That's exactly what EmDash is: a full-stack, Astro-native CMS that carries forward WordPress's best ideas (extensibility, admin UX, plugin ecosystem) on a modern, type-safe foundation.
Launched in April 2026 by a team of former WordPress contributors and Astro core members, EmDash has already gained 10,700+ GitHub stars. Its killer feature is sandboxed plugins — plugins run in isolated Worker environments, each with a declared capability manifest, solving the #1 security problem that has plagued WordPress for two decades.
In this guide, you'll learn what makes EmDash different, how to self-host it with Docker, and how to migrate your existing WordPress site.
Why EmDash Is Trending
EmDash exploded on GitHub because it addresses the pain points that have accumulated across two decades of WordPress dominance:
- Security: 96% of WordPress vulnerabilities come from plugins. EmDash plugins run in sandboxed Worker isolates with explicit capability declarations — a plugin that requests
read:contentandemail:sendcan do exactly that and nothing else. - Structured content: WordPress stores rich text as HTML with metadata embedded in comments. EmDash uses Portable Text — a structured JSON format that decouples content from presentation. Your content renders as a web page, mobile app, email, or API response without parsing HTML.
- Type safety: Content types are defined in the database, not in code. EmDash auto-generates TypeScript types from your live schema. Full type safety and autocomplete from query to template.
- Agent-ready: Built-in MCP server, CLI for programmatic management, and skill files for AI-assisted plugin development.
- Portable: Runs on Cloudflare (D1 + R2 + Workers) or any Node.js server with SQLite. Same codebase, no lock-in.
Architecture Overview
EmDash is structured as a layered system. At the top, the Astro application (your site) integrates EmDash as a plugin. Below it, the EmDash Core handles routing, authentication, and the admin panel. The Storage Layer abstracts the database (SQLite, D1, PostgreSQL) and file storage (local, S3, R2) behind portable interfaces. Plugins run in isolated sandboxes with capability manifests, communicating via lifecycle hooks. The MCP Server exposes content management to AI agents, and the CLI handles migrations, type generation, and content operations.
Key Components
- EmDash Core (
emdash/astro) — The Astro integration that adds the admin panel, REST API, auth, media library, and plugin system to any Astro project - Portable Text Engine — Structured content storage using the Portable Text JSON format, decoupling content from its HTML representation
- Plugin Sandbox — Isolated Worker environments (Dynamic Worker Loaders on Cloudflare, in-process safe mode on Node.js) with capability-based security
- Database Abstraction — Kysely-based SQL abstraction supporting SQLite, D1, Turso/libSQL, and PostgreSQL
- Storage Abstraction — S3-compatible API supporting local filesystem, R2, and AWS S3
- MCP Server — Built-in Model Context Protocol server for AI tool integration
- Auth System — Passkey-first (WebAuthn) with OAuth and magic link fallbacks, RBAC with four roles
- WordPress Migration Engine — Import from WXR exports, REST API, or WordPress.com
Prerequisites
Before you begin, make sure you have:
- Node.js 20+ and npm / pnpm installed
- Docker and Docker Compose (for the self-hosted setup)
- A domain name (if deploying to production)
- Basic familiarity with Astro and TypeScript
Installation and Setup
Quick Start with npm (No Docker)
The fastest way to try EmDash is with the Astro template:
# Create a new EmDash blog project
npm create astro@latest -- --template @emdash-cms/template-blog
cd my-emdash-blog
npm install
npm run dev
Open http://localhost:4321/_emdash/admin to access the admin panel. The first-run wizard will guide you through creating an admin account.
Local Development with SQLite (No Cloudflare Required)
EmDash works great with local SQLite for development. Make sure your astro.config.mjs is set up:
// astro.config.mjs
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
integrations: [emdash({ database: sqlite() })],
});
Generate TypeScript types from your content schema:
npx emdash types
Query content in your Astro components with full type safety:
---
import { getEmDashCollection } from "emdash";
const { entries: posts } = await getEmDashCollection("posts");
---
{posts.map((post) => <article>{post.data.title}</article>)}
Self-Hosted Docker Setup
For production, here's a complete Docker Compose setup with SQLite (no external database needed):
# docker-compose.yml
version: "3.8"
services:
emdash:
image: node:22-alpine
container_name: emdash
restart: unless-stopped
working_dir: /app
ports:
- "4321:4321"
volumes:
- ./app:/app
- emdash_data:/app/data
environment:
- NODE_ENV=production
- EMDASH_SECRET=change-this-to-a-random-secret
- EMDASH_DB_TYPE=sqlite
- EMDASH_DB_PATH=/app/data/emdash.db
- EMDASH_STORAGE_TYPE=local
- EMDASH_STORAGE_PATH=/app/data/media
- EMDASH_URL=https://yourdomain.com
command: >
sh -c "
npm install &&
npx emdash build &&
npx emdash start
"
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:4321/_emdash/admin"]
interval: 30s
timeout: 10s
retries: 3
volumes:
emdash_data:
But the recommended approach is to build your Astro site and serve it with nginx:
# Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx emdash build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Environment Variables
| Variable | Description | Default |
|---|---|---|
EMDASH_SECRET |
Secret key for encryption and sessions | Required |
EMDASH_DB_TYPE |
Database type: sqlite, d1, postgres |
sqlite |
EMDASH_DB_PATH |
Path to SQLite database file | ./data/emdash.db |
EMDASH_STORAGE_TYPE |
Storage backend: local, r2, s3 |
local |
EMDASH_URL |
Public URL of your EmDash site | Required for production |
WordPress Migration
One of EmDash's standout features is the WordPress import wizard. It supports three migration paths:
1. WXR Export Import
# Export from WordPress admin: Tools → Export → All content
# Then import into EmDash:
npx emdash import:wxr ./wordpress-export.xml
2. REST API Migration
npx emdash import:rest https://old-wordpress-site.com/wp-json
3. WordPress.com Import
npx emdash import:wpcom your-wordpress-com-site
The migration engine imports posts, pages, media, categories, tags, and navigation menus. EmDash also provides agent skill files that help port WordPress plugins to the EmDash plugin API.
Plugin Development
EmDash plugins use a clean definePlugin() API with capability manifests:
// my-plugin.ts
export default () =>
definePlugin({
id: "notify-on-publish",
capabilities: ["read:content", "email:send"],
hooks: {
"content:afterSave": async (event, ctx) => {
if (event.content.status !== "published") return;
await ctx.email.send({
to: "editors@example.com",
subject: `New post: ${event.content.title}`,
});
},
},
});
Available hook points include:
content:beforeSave/content:afterSave— Content lifecycleuser:afterLogin/user:afterRegister— Authentication eventsadmin:beforeRender— Admin UI customizationapi:beforeResponse— API middleware hooks
MCP Integration
EmDash ships with a built-in MCP server, enabling AI tools like Claude, ChatGPT, and Cursor to interact with your CMS directly:
# Start the MCP server
npx emdash mcp-server
This exposes content operations as MCP tools: listCollections, getPosts, createPost, updatePost, deletePost, searchContent, and manageMedia. AI agents can create and edit content programmatically.
Comparison: EmDash vs WordPress vs Astro Content Collections
- Plugin security: EmDash — Sandboxed isolates. WordPress — Full access. Astro Content Collections — N/A.
- Content format: EmDash — Portable Text (JSON). WordPress — HTML + comments. Astro Content Collections — Markdown/MDX.
- Type safety: EmDash — Generated TypeScript. WordPress — None. Astro Content Collections — Manual types.
- Database: EmDash — SQLite / D1 / PostgreSQL. WordPress — MySQL/MariaDB. Astro Content Collections — Filesystem.
- Admin UI: EmDash — Built-in visual builder. WordPress — Built-in. Astro Content Collections — None.
- MCP / Agent support: EmDash — Native. WordPress — Via plugins. Astro Content Collections — None.
- Deployment: EmDash — Node.js / Cloudflare. WordPress — LAMP stack. Astro Content Collections — Static/SSR.
- Plugin system: EmDash — definePlugin() API. WordPress — WP Plugin API. Astro Content Collections — N/A.
Verification Checklist
After setting up your EmDash instance, run through this checklist:
- ✅ Admin panel accessible at
/_emdash/admin(200) - ✅ Able to create a new content collection
- ✅ Rich text editor loads and saves content correctly
- ✅ Media upload works (drag-and-drop)
- ✅ Public site renders content from the CMS
- ✅ Type generation works:
npx emdash types - ✅ MCP server starts and exposes tools
- ✅ Passkey / OAuth login works
- ✅ WordPress import completes without errors
Resources
- GitHub: github.com/emdash-cms/emdash
- Documentation: docs.emdashcms.com
- Website: emdashcms.com
- Starter Templates: github.com/emdash-cms/templates
- Portable Text Spec: portabletext.org
- Astro Documentation: astro.build