Running EmDash CMS in Production One Day After It Launched

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, what EmDash’s 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.
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 entire installation, in our astro.config.mjs, verbatim:
emdash({
database: d1({ binding: 'DB' }),
storage: r2({ binding: 'MEDIA' }),
mcp: true,
}),
Three lines gets you an admin panel at /_emdash, a REST API, authentication, a media library, a plugin system, and an MCP server, all deployed inside your own Worker. Cloudflare says it built EmDash in about two months using agentic AI. We migrated onto it in an afternoon using agentic AI.
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 admin UI 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 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. And the marketing team edits in /_emdash; nobody files a pull request to fix a typo anymore.
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, betting on someone’s developer preview, or building the team that can make bets like this safely, I’d enjoy comparing notes. You can find me on LinkedIn or X, and Prepa IN at prepain.mx.
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.
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.