← Back to all posts

Cloudflare EmDash CMS: Open Source Successor to WordPress

Updated: November 2, 2026
Cloudflare EmDash CMS: Open Source Successor to WordPress

Cloudflare launched EmDash as a developer preview, pitching it as “the spiritual successor to WordPress”. The coverage was polite but clear: “not yet suitable for migrating production WordPress sites.”

We had it running Prepa IN’s production blog by 8:17 the next evening.

This post is why we made that bet, how EmDash getting started and EmDash admin work in practice, what the architecture looks like from inside, and the part I’m proudest of: the scaling bug we found at 124 posts and fixed in EmDash core, merged upstream nine days after it took our blog down.

Why we went back to a CMS

When we migrated Prepa IN off WordPress in a weekend, content landed as markdown in git. For a developer that’s a dream. For everyone else it’s a locked door: every typo fix was a pull request.

The requirement was narrow: something a non-developer can edit, that doesn’t drag a PHP host into a Cloudflare Workers stack, and that doesn’t put six years of content behind someone else’s API. And one requirement that decided it: the CMS had to be AI-first. By then most of our content operations were run by agents, and every CMS we evaluated treated that as an integration problem, an API key here, a webhook there. I wanted a CMS where almost everything you would normally do in the admin UI, an agent could do natively, with real permissions. When we went looking, that category barely existed. Then Cloudflare shipped one.

Getting started with EmDash

Official EmDash getting started is documented at docs.emdashcms.com. Two paths land in the same place: a scaffolded site, or the Astro integration on an existing project.

Scaffold a site. Node.js 22.23.4 or later. From the docs:

npm create emdash@latest

Choose a deploy target and template (for example Node starter, or Cloudflare blog via --template cloudflare:blog). The scaffolder writes a gitignored .env with EMDASH_ENCRYPTION_KEY. Start the dev server, open /_emdash/admin, and finish the setup wizard: site title, admin email/name, then register a passkey. Sample content and the content model come from the template seed when you leave that option selected.

Add EmDash to an existing Astro app. The project needs output: "server", an adapter, React (the admin UI is a React app), a database adapter, and media storage. Cloudflare’s path matches what we run: D1 for SQL and R2 for media. Astro Live Collections wire through emdashLoader() so pages read published content at request time.

Our production install, in astro.config.mjs, verbatim:

emdash({
  database: d1({ binding: 'DB' }),
  storage: r2({ binding: 'MEDIA' }),
  mcp: true,
}),

Those three lines get you EmDash admin at /_emdash/admin, a REST API, authentication, a media library, a plugin system, and an MCP server, all inside your own Worker. After deploy, complete the same admin setup on the live URL. Developers who change the content model in admin can run npx emdash types to generate TypeScript from the live schema.

Cloudflare says it built EmDash in about two months using agentic AI. We migrated onto it in an afternoon using agentic AI. The rest of this post is what that afternoon committed us to.

EmDash admin

EmDash admin is the editing interface served by your Astro app at /_emdash/admin/. It uses the same database, media storage, content model, and plugins as the public site. The first visit on a new site opens the setup wizard; after that, the signed-in role decides what you see.

From the admin panel docs, the main work areas are:

  • Dashboard for site and plugin widgets
  • Collections such as Posts: list, create, edit, publish, trash
  • Media for upload, search, replace, and delete
  • Taxonomies and bylines for organization and credited authors
  • Menus and widget areas when the theme queries them
  • Content types to create collections and fields
  • Plugins and Settings (site, auth, backups, email, search, and related services)

Roles are Subscriber, Contributor, Author, Editor, and Admin. Permissions stack upward; ownership still matters for content and media. Editors can manage other authors’ content and several editorial surfaces; Admins can change the content model, users, plugins, redirects, backups, and settings, and can permanently delete trashed content. Hiding a nav item is not the security boundary; the server checks each action.

For draft-enabled collections, Save and Publish are separate. Save stores a draft; Publish (or Publish changes) makes that version visible to public queries. Scheduling records when a draft should go live. Edit locking, when enabled, refuses concurrent overwrites.

That is the EmDash admin our marketing team uses at Prepa IN: they open /_emdash/admin, edit Portable Text in the block editor, attach media, and publish. Nobody files a pull request to fix a typo anymore. Agents reach the same capabilities through the MCP server behind scoped OAuth (content:*, media:*, schema:*, and admin), which is why the admin UI and the agent surface stay peers rather than a bolted-on API key.

What EmDash is

EmDash is a full-stack TypeScript CMS, MIT-licensed and open source, built by Cloudflare: Astro-first, running on Workers with D1 for SQL and R2 for media, and containing zero WordPress code.

The architecture, reviewed

I’ve now lived inside this codebase as both a consumer and a contributor, so here is the review I wish I’d had on launch day.

It’s a dependency, not a platform. EmDash is an Astro integration in your package.json, running inside your deployment, on your Cloudflare account. No EmDash cloud sits between you and your content. This sounds like a detail. It’s what made the contribution story below possible.

The schema lives in the database, not in code. Collections created in the EmDash admin become real SQL tables with typed columns; developers run npx emdash types to generate TypeScript from the live schema and query through Astro’s Live Collections with no rebuilds. It’s the right inversion: the people who change content models are rarely the people who deploy code.

Content is Portable Text, not serialized HTML, so the same content renders as a page, a feed, an email, or an API response without HTML parsing.

Plugins are sandboxed with declared capabilities. Cloudflare’s pitch, citing Patchstack’s data, is that 96% of WordPress vulnerabilities come from plugins with full access to everything. EmDash plugins run in isolated Worker sandboxes, each with a capability manifest: a plugin that declares read:content and email:send can do exactly that and nothing else. One caveat: the sandbox requires a paid Workers plan.

And it’s portable by design. SQL goes through Kysely, storage speaks the S3 API; it runs best on D1 and R2 but also on SQLite, Turso, Postgres, or local files. The MCP server, agent skills, and CLI are peers of the EmDash admin UI.

What I’d flag to a CTO: the schema-in-database model means your content model isn’t in version control (bring your own seed and migration discipline), and a v0.x release cadence on the dependency rendering your entire site is a recurring operational cost. Both were prices we paid knowingly.

The bug we found, and fixing it in core

Two days in, the blog stopped loading.

Cloudflare D1 caps bound parameters at 100 per query. EmDash hydrated bylines for an entire collection in a single IN (?, ?, ...) clause, one variable per post. Our corpus crossed 124 posts, and every page that listed content began failing with:

D1_ERROR: too many SQL variables at offset 369: SQLITE_ERROR

With WordPress, this is where you file a support ticket against a plugin and start refreshing a changelog. Here the CMS was TypeScript in my own node_modules. So: read the source, write the fix, carry it locally with patch-package, ship production the same day, send the fix upstream.

The fix is the kind of boring that survives review: a shared chunks() helper, a SQL_BATCH_SIZE of 50, IDs deduplicated so a record crossing a chunk boundary can’t be credited twice. While writing it I found the same unbounded-IN pattern in the SEO metadata repository, which fails even earlier. Two pull requests, #223 for bylines and #422 for SEO, with ten new tests between them. Both merged into emdash-cms/emdash within the week; two days after that we deleted our local patches, because the next upstream release shipped everything we’d been carrying. Nine days from “the blog is down” to “the fix is in the product,” with production never down past the first hour.

That’s the argument this post exists to make. A bug in a WordPress plugin is a ticket into someone else’s queue. A bug in a CMS that’s a readable dependency is a branch. There is a prerequisite: someone on your side has to read TypeScript. But that’s exactly the capability a technical founder should be hiring for anyway.

What mcp: true bought us

That one flag turns the CMS into a surface agents can operate. Not an API key taped to the side: a full OAuth 2.0 resource server, with authorization code, refresh token, and device code flows, PKCE, and scoped permissions:

content:read   content:write
media:read     media:write
schema:read    schema:write
admin

On that surface we built the autonomous editorial pipeline I wrote up in how a fleet of Claude managed agents grew Prepa IN’s SEO and AI visibility 20x. The editorial standards became executable: “every post needs a hero image, a byline, an SEO title and description” stopped being a document nobody reads and became a reviewer that fails closed. Editors still work in EmDash admin; agents work the same permissions model through MCP.

The risk calculus

We took every release as it came, roughly a minor version a month on the dependency rendering the whole site. That pace isn’t free, but every upgrade has arrived carrying fixes we wanted, twice including our own.

This was the right bet because the blog is recoverable. The content lives in D1 and R2 in our own account with a seed file in the repo, and the abstractions are portable by design. If EmDash had died on us, the exit was a script, not a hostage negotiation. Betting a checkout flow or our student platform on a day-old preview would have been reckless. Betting a blog on one was a calculated risk with a known floor.

What I’d tell a founder or CTO weighing something similar:

Price the floor, not the label. “Developer preview” describes the software’s maturity, not your risk. Your risk is the recovery path. Ours was short and tested; that’s what made day-one adoption sane.

Choose dependencies someone on your team can read. Every advantage in this story, from the first patch to the merged PRs, came from the CMS being readable TypeScript in our own tree. Open source you can’t read is just a slower vendor.

Evaluate a CMS by what an agent can do in it unattended. Scoped OAuth for agents, an MCP surface, a schema an agent can safely touch: those checks are what put EmDash ahead of everything else we evaluated.

The full arc, in one line: WordPress, then markdown in git, then EmDash on D1. We left a CMS, spent three months without one, and came back to a different kind: one our agents can operate, and one we can send a pull request to.

If you’re evaluating EmDash, working through EmDash getting started, comparing EmDash admin to the stack you have, or building the team that can make bets like this safely, I’d enjoy comparing notes. I’m Angel Baez, Applied AI Engineer and co-founder of Prepa IN and Recupera. You can find me on LinkedIn or X, and Prepa IN at prepain.mx.

Also available in Spanish.

Frequently asked questions

What is EmDash CMS?

EmDash is Cloudflare's open-source, MIT-licensed TypeScript CMS, pitched as the spiritual successor to WordPress. It installs as an Astro integration and runs on Cloudflare Workers with D1 for SQL and R2 for media, with portable abstractions that also work with SQLite, Turso, Postgres, and S3. Content is stored as Portable Text, plugins run in sandboxed Worker isolates with declared capabilities, and it ships with a built-in MCP server so AI agents can operate the CMS directly.

Is EmDash ready for production?

Launch coverage called the developer preview "not yet suitable for migrating production WordPress sites." We still ran Prepa IN's production blog on it the next day as a calculated risk with a known floor: content lives in D1 and R2 in our own Cloudflare account with a seed file in the repo, and EmDash's abstractions are portable by design, so the worst case was recoverable. We would not have made the same bet on a checkout flow or the student platform. The post also flags a recurring operational cost from a v0.x release cadence, and a scaling bug we hit at 124 posts that we fixed upstream.

What is EmDash MCP?

EmDash ships with a built-in MCP server. Setting mcp: true on the Astro integration turns the CMS into a surface agents can operate: a full OAuth 2.0 resource server with authorization code, refresh token, and device code flows, PKCE, and scoped permissions (content:read, content:write, media:read, media:write, schema:read, schema:write, and admin). The MCP server, agent skills, and CLI are peers of the EmDash admin UI, so almost anything you would do in the admin an agent can do with scoped, revocable permissions.

Can you self-host EmDash?

Yes. EmDash is an Astro integration in your package.json that runs inside your own deployment on your Cloudflare account; there is no EmDash cloud between you and your content. On Cloudflare it uses Workers with D1 for SQL and R2 for media, and it is portable by design to SQLite, Turso, Postgres, and S3-compatible or local storage.

How do I get started with EmDash?

Scaffold with npm create emdash@latest (Cloudflare blog or Node starter templates), or add the emdash Astro integration to an existing server-rendered Astro project with a database and media storage. Run the dev server, open /_emdash/admin, and finish the setup wizard (site title, admin account, passkey). On Cloudflare, configure D1 and R2 bindings, deploy the Worker, then complete the same admin setup on the live URL. Official docs: docs.emdashcms.com.

What is the EmDash admin?

The EmDash admin is the React editing UI served at /_emdash/admin inside your Astro app. After the setup wizard, signed-in roles (Subscriber through Admin) control which areas appear: dashboard, collections such as Posts, media, taxonomies and bylines, menus, content types, plugins, and settings. Save and Publish are separate for draft-enabled collections. At Prepa IN, marketing edits content there while agents use the MCP surface with scoped OAuth, including an admin scope.

What does an AI-first CMS actually mean?

The CMS treats agents as first-class users. EmDash exposes everything an admin can do through a built-in MCP server behind scoped OAuth, plus agent skills and a CLI. Almost anything you would do in the admin UI, from editing content to changing the schema, an agent can do with scoped, revocable permissions.

Was adopting a day-old developer preview in production reckless?

It was a calculated risk with a known floor. The content lives in D1 and R2 in our own Cloudflare account with a seed file in the repo, and EmDash's abstractions are portable by design, so the worst case was recoverable. We would not have made the same bet on a checkout flow or the student platform.

What was the bug you found and contributed the fix for?

Cloudflare D1 caps bound SQL parameters at 100 per query, and EmDash hydrated bylines and SEO metadata for a whole collection in single IN (...) clauses. At 124 posts, our blog stopped loading with D1_ERROR: too many SQL variables. We patched it locally, then upstreamed two fixes (PRs #223 and #422) that chunk those queries in batches of 50, with tests. Both merged into emdash-cms/emdash within days.