Real-Time Updates
CWA is live by default. When an admin publishes a component, every open browser session is told about it via the Mercure hub — no page reload, no polling, no WebSocket code on your end.
How It Works
- An admin publishes a component via the CMS
- The Symfony API sends an update to the Mercure hub (because
mercure: trueis on the entity) - The hub broadcasts the update to all subscribed browser connections
- The Nuxt module receives the event and stages the new resource in the store — the rendered page is untouched
- A small "content is outdated" notice appears
- When the reader accepts,
mergeNewResources()applies the staged resources and Vue re-renders
You write none of this plumbing. It's in the module.
Prerequisites
PHP Side
Add mercure: true to the #[ApiResource] attribute on any entity you want to broadcast:
#[ApiResource(mercure: true)]
class Title extends AbstractComponent { ... }
Without this, the resource updates silently (the API saves the change, but no Mercure event fires).
Infrastructure
Mercure runs as a Caddy module inside the FrankenPHP php container — there is no separate hub service to run or deploy. The template's Compose file explicitly suppresses the Flex recipe's standalone hub service for this reason.
The relevant environment variables, with the template's defaults:
# API → hub (internal, server-side)
MERCURE_URL=http://php.local/.well-known/mercure
# Browser → hub (public-facing)
MERCURE_PUBLIC_URL=https://localhost/.well-known/mercure
# Shared secret — set once, used for both the publisher and subscriber JWT keys
CADDY_MERCURE_JWT_SECRET=your_secure_secret
In production, MERCURE_PUBLIC_URL becomes your public domain plus /.well-known/mercure — it is derived from SERVER_NAME by default. Override the two URLs with CADDY_MERCURE_URL and CADDY_MERCURE_PUBLIC_URL if you need to.
No Front-End Code Needed
The module subscribes to the Mercure hub in its Nuxt plugin, and filters what arrives against the resources on the current page. You never write EventSource or WebSocket code.
The subscription lifecycle:
- On page load: the module opens a single
EventSourceconnection toMERCURE_PUBLIC_URL - Topics: one wildcard subscription (
?topic=*) — incoming messages are then matched againstcwa.resources.currentIdsclient-side, and anything for another page is discarded - On navigation: the subscription URL is unchanged; only the client-side filter moves with you
- On sign-in: Mercure re-initialises so private topics (admin resources) are included
* topic for subscriber tokens, and every update published anywhere on the site is delivered to every connected browser before being filtered. Worth knowing when sizing the hub.The Visitor Notification
When a Mercure update arrives for a resource on the current page, a small notice appears reading The content on this page is outdated, with an Update button. Clicking it calls mergeNewResources(), which applies the staged resources and re-renders.
There is no dismiss control — the notice disappears once the update is applied. Admins get the same notice, rendered inside the admin header rather than floating over the page.
Troubleshooting
No live updates arriving in the browser:
- Check that
MERCURE_PUBLIC_URLis reachable from browsers — not an internal Docker hostname - Confirm
mercure: trueis on the PHP entity - Open DevTools → Network → Filter by
EventSource— you should see a persistent connection to the hub - Check the hub logs for authentication errors
Hub 401 errors:
CADDY_MERCURE_JWT_SECRETfeeds bothMERCURE_PUBLISHER_JWT_KEYandMERCURE_SUBSCRIBER_JWT_KEY— check it is set (and identical) everywhere thephpservice is configured, includingcompose.prod.yaml
Updates arrive but the component doesn't re-render:
- Confirm the component uses
useCwaComponent(oruseCwaResource) — both react to store updates - Bare
$fetchcalls are not reactive; usecwa.resources.getResource(iri)for reactive lookups
Advanced: Acting on Other Updates
The module's own subscription is already a wildcard, so there is no extra topic to register — everything published to the hub reaches the browser. What the module doesn't do is act on messages for resources outside the current page: those are filtered out and dropped.
If you need to react to them (a live "new comment" counter, say), open your own EventSource in a Nuxt plugin alongside the module's. The real constraint is hub-side: your subscriber tokens must be authorised for the topics you want.
The resources store is reactive, so anything you write into it re-renders the components bound to that IRI, regardless of which connection delivered it:
// Stage it, exactly as the module's own Mercure handler does
$cwa.resourcesManager.saveResource({ resource, isNew: true, path })
// Or write it straight into the current page state
$cwa.resourcesManager.saveResource({ resource })
There is no built-in CWA API for registering custom topics today.