That is not a rhetorical warm-up. WordPress runs an enormous share of the web
because it is free, extensible, and staffed by a talent pool no competitor can
match. If one team edits one site, the theme does what you need, and traffic is
steady — a migration will cost you months and buy you very little.
This article is about the cases where that stops being true.
WordPress can expose content over the REST API or WPGraphQL, and plenty of
teams ship this successfully. But it is worth being precise about what you are
running.
WordPress's core job is to render a page: query the database, run the theme,
fire the hooks, return HTML. Going headless means keeping that machinery,
booting it on every request, and then throwing away its output in favour of a
JSON response you assemble separately.
You end up operating two systems in one process. The admin is a page renderer.
The API is a layer on top of a page renderer. Plugins — which are the reason
you chose WordPress — were written for the page renderer, and a plugin that
adds a field to the editor does not necessarily add it to the API.
corpusctl never renders a page. There is no theme layer, no template hierarchy,
no hook order to reason about. The read client is the primary interface, not a
bolt-on.
#What actually happens when someone reads your post
WordPress: the request reaches PHP, PHP queries MySQL, plugins run, the
theme renders. You then put a caching plugin in front, then perhaps a CDN in
front of that, and you spend real time on cache invalidation — because the page
is computed and the cache is a copy of a computation that could change at any
moment.
corpusctl: publishing runs the build pipeline. Content is validated,
rendered, and written to an immutable, hash-addressed file in object
storage; the manifest is refreshed. A visitor's client resolves the slug to a
manifest shard locally, then fetches that address from the nginx edge cache.
Because the address is a content hash, the same address can never return
different content — so it is cacheable for thirty days without an invalidation
strategy. Publishing writes a new address. There is nothing to purge.
Two HTTP requests, both from cache. Neither reaches the API. Neither reaches
PostgreSQL.
The number that follows: visitor traffic does not load your write path at all.
An editor saving a draft during a traffic spike is not competing with the spike.
The plugin ecosystem is WordPress's greatest strength, and it is the same
sentence as its greatest operational risk. Every plugin is third-party code
running in your process with database access. A site with thirty plugins has
thirty update streams and thirty potential CVEs, and the plugin that breaks is
usually the one nobody remembers installing.
corpusctl has no plugin marketplace — which is a real loss, listed honestly
below. What it has instead is a module system with a rule enforced by CI:
Every feature is a module: media, search, webhooks, SEO, GraphQL, import,
analytics.
Modules may not import each other. A service registry and an event bus
are the only channels between them, and pnpm check:isolation fails the
build if that rule is broken.
Removing a module cannot break the build. That is the entire point of the
rule.
And turning a feature off does not destroy anything: disabled preserves the
data and resumes on re-enable, uninstalled clears it with a double
confirmation, and a plan downgrade only ever sets modules to disabled.
WordPress Multisite exists and it works, with the caveats operators know:
plugins that misbehave across the network, a shared database, an upgrade that
lands on every site at once, and a permissions model that was not designed for
"this editor works on brands one and three but must not see brand two".
corpusctl is multi-tenant in the core, next to auth and the module
registry:
One panel, unlimited spaces.
A user carries a separate role per space.
Isolation enforced by PostgreSQL row-level security — in the database,
below the application layer where a forgotten filter lives.
The argument for RLS is narrow: application-level isolation holds until the one
query that skipped the filter, and that query gets written eventually, by
someone in a hurry, in a module nobody reviews closely. RLS makes the leak
impossible underneath the mistake.
WordPress content is a post or a page, extended with custom fields and a plugin
to manage them. It works, and the model shows its origins.
In corpusctl there are no built-in content types. You define them from the
panel or the API, and fields carry roles — title, slug, body — so the
system knows which field is the heading without depending on what you named it.
Localisation is per field. Every record is versioned, older versions are
restorable, and publishing can be scheduled.
On the front end, block renderers ship with zero default styling. Not one
line of CSS. Every block type is overridable, and an unknown block type is
skipped gracefully rather than throwing. The core render tree is
framework-agnostic; React, Vue and Next.js adapters sit on top as thin layers.
Compare that to a WordPress theme, which is a design decision you inherit and
then override.
Hiring. You can find someone who knows WordPress in any city, at any
budget, next week. You cannot do that for corpusctl and pretending otherwise
would be silly.
The ecosystem. Forms, bookings, memberships, e-commerce, SEO tooling,
migration utilities — a plugin exists, it is battle-tested, and it costs $59.
Building the equivalent against an API is a project.
It is free. WordPress core costs nothing and always has. If budget is the
binding constraint, that ends the discussion.
Add a fourth, less comfortable one: corpusctl is young. SSO and a
contractual SLA are on the roadmap, which is the polite way of saying they do
not exist yet. There is no marketplace and no global CDN — the edge cache is
single-region. If your procurement checklist has any of those on it, this
comparison ends here.
Stay on WordPress if one team edits one site, the ecosystem is doing real work
for you, or budget decides it.
Move to corpusctl if you run several projects, several front ends, or a plugin
surface you can no longer audit — and you want the read path to stop being a
caching problem.
Migrating? The import module reads a WordPress WXR export: posts, pages,
tags and media. Field roles are mapped, and the body is converted to the block
model. Budget real time for anything a plugin was doing — that logic does not
export.