The CWA is in heavy development
The CWA is still in alpha and not ready for production - some code and implementations are likely to change. If you would like to try out the CWA, please enjoy what we have provided and feel free to provide feedback, or get involved on GitHub.
DraftComponent Helpers

Real-Time Updates

How Mercure broadcasts live content changes to all open browser sessions automatically when a component is published.

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

  1. An admin publishes a component via the CMS
  2. The Symfony API sends an update to the Mercure hub (because mercure: true is on the entity)
  3. The hub broadcasts the update to all subscribed browser connections
  4. The Nuxt module receives the event and stages the new resource in the store — the rendered page is untouched
  5. A small "content is outdated" notice appears
  6. When the reader accepts, mergeNewResources() applies the staged resources and Vue re-renders

You write none of this plumbing. It's in the module.

Staging is deliberate: content is never swapped out from under someone mid-read. Nothing on the page changes until the update is accepted.

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 EventSource connection to MERCURE_PUBLIC_URL
  • Topics: one wildcard subscription (?topic=*) — incoming messages are then matched against cwa.resources.currentIds client-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
Because the subscription is a wildcard, your hub must allow the * 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:

  1. Check that MERCURE_PUBLIC_URL is reachable from browsers — not an internal Docker hostname
  2. Confirm mercure: true is on the PHP entity
  3. Open DevTools → Network → Filter by EventSource — you should see a persistent connection to the hub
  4. Check the hub logs for authentication errors

Hub 401 errors:

  • CADDY_MERCURE_JWT_SECRET feeds both MERCURE_PUBLISHER_JWT_KEY and MERCURE_SUBSCRIBER_JWT_KEY — check it is set (and identical) everywhere the php service is configured, including compose.prod.yaml

Updates arrive but the component doesn't re-render:

  • Confirm the component uses useCwaComponent (or useCwaResource) — both react to store updates
  • Bare $fetch calls are not reactive; use cwa.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.