4.2 KiB
ADR-WS-003: STOMP is an Advanced adapter with a declared destination catalog
- Status: Accepted
- Date: 2026-08-25
- Scope:
adapter:inbound:websocket—advanced.stomp,advanced.stomp.rabbit
Context
Advanced Tasks 9–13 add STOMP 1.2 alongside the platform's own protocol, plus a RabbitMQ broker relay and cross-node user destinations.
This leaf already ships an older STOMP-over-SockJS channel (stomp, gated on
ca-skeleton.websocket.enabled). Two @EnableWebSocketMessageBroker configurations in one context
do not conflict loudly — both contribute a configurer, both call configureMessageBroker, and the
broker that results is whichever ran last. Nothing errors and nothing logs.
STOMP also brings a destination model that is a free string from the client. Without a catalog, the set of reachable destinations is whatever the broker accepts, which for the simple broker is every string.
Decision
The Advanced adapter is its own module (advanced-stomp), separate from advanced. It is the
one Advanced capability that cannot be pure — STOMP here is the Spring Messaging types — and
folding it into advanced would relax that module's purity for every capability in it.
The relay is a further module (advanced-stomp-rabbit). The adapter parses a protocol; the
relay opens a TCP connection to somebody else's broker and makes every delivery depend on it.
Different blast radius, different decision, different module.
Destinations are declared, per operation. StompDestinationCatalog maps (operation, destination) to a required permission. Undeclared is refused. SUBSCRIBE and SEND are separate
declarations, because reading a feed and publishing into it are different rights.
The authorization decision is a value, not an interceptor method. StompAuthorizationPolicy
returns a StompAuthorizationDecision; StompSecurityInterceptor only extracts and enforces. A rule
reachable only through a MessageChannel gets tested for the cases somebody built a channel for.
Only one STOMP runtime may run. StompBrokerExclusivity fails the context when both channels
are enabled, when both brokers are, or when the adapter is enabled with no broker behind it.
A RECEIPT is never promoted to a commit. StompEvidence has six stages and
StompAckPolicy.evidenceForReceipt() is fixed at PROTOCOL_RECEIPT. The receipt is written by the
protocol layer, which knows nothing about whether the work succeeded.
The simple broker declares what it cannot do. SimpleBrokerProfile cannot be constructed
claiming cluster support or durable acks, and refuses activation outside local/test — in a
multi-node deployment it does not error, it delivers to whichever fraction of users is on the
publishing node.
Unresolved user destinations are broadcast once and then dead-lettered.
MultiNodeUserDestination distinguishes a message that arrived via the broadcast from one that did
not. Without that, every node rebroadcasts every unresolvable message on receipt.
Consequences
- Enabling Advanced STOMP requires disabling the legacy channel. There is no migration path that runs both; the exclusivity check makes that explicit at startup rather than at 3am.
- A deployment must write its own catalog. There is deliberately no default: an empty one refuses every frame and reads as a broken adapter, and a non-empty one publishes destinations nobody chose.
- The relay's cost is one broker connection per authenticated session plus one system connection.
brokerConnectionsForexists so this is computed before the first outage. - User-destination metrics are tagged with
UserDestinationAction, never the destination — a user destination contains a user identifier by construction.
Alternatives considered
- Extend the existing
stomppackage. Rejected: it is Stable, and WS-ARCH-6 forbids a Stable module naming an Advanced one. Making the legacy channel profile-driven would have required that edge. - One
advanced-stompmodule including the relay. Rejected: the relay is a separate operational decision and deserves to be refusable on its own. - Allow undeclared destinations with a wildcard permission. Rejected: the wildcard becomes the default and the catalog becomes documentation.