KubeMQ
ConnectorsSTOMP

STOMP

Point a STOMP app at KubeMQ by changing only the broker address — all five KubeMQ patterns over the STOMP 1.0/1.1/1.2 wire, selected by destination prefix.

Point your existing STOMP application at KubeMQ by changing only the broker address. The STOMP connector is a built-in, wire-protocol bridge inside kubemq-server — an embedded STOMP server with its own dedicated TCP/TLS listeners and a hand-rolled frame codec. Any standard, unmodified STOMP client (go-stomp, stomp.py, Stomp.Net, Spring) talks to KubeMQ's Queues, Events, Events-Store, Commands, and Queries with no code change, no library swap, and no KubeMQ SDK.

What is the STOMP connector

STOMP (Simple Text Oriented Messaging Protocol) is a frame-based text protocol: a client opens a connection with a CONNECT frame, then SENDs to and SUBSCRIBEs on destinations. The KubeMQ STOMP connector negotiates STOMP 1.0, 1.1, and 1.2 (it picks the highest common version; the examples default to 1.2) over raw TCP, and maps the STOMP wire protocol onto KubeMQ's five native messaging patterns by destination prefix.

The first path segment of a destination selects the pattern; the remaining segments are joined with . into the KubeMQ channel — /topic/orders/new becomes Events channel orders.new. The connector is a gateway, not a client library: your application only needs a stock STOMP client.

Key capabilities:

  • All five patterns over one wire — Queues, Events, Events-Store, Commands, and Queries, selected by the destination prefix.
  • ActiveMQ-style primary names, MQTT-style aliases — lead with /queue/, /topic/, /topic-store/, /command/, /query/; the aliases /queues/, /events/, /store/, /commands/, /queries/ resolve to the same patterns, and egress always canonicalizes back to the primary name.
  • RPC requester-only — a STOMP client SENDs to /command/ or /query/ and receives the reply on a connection-local /reply/ subscription; the responder runs on the KubeMQ (gRPC) side.
  • Cross-protocol interop — a message sent over STOMP to /topic/orders/new is consumable by a gRPC or REST KubeMQ client on channel orders.new, and vice-versa.

How it works

A STOMP client connects to the connector and SENDs to a destination. The connector resolves the destination to a KubeMQ (pattern, channel) pair, hands the message to the message broker, and consumers on the same channel — over STOMP or any other KubeMQ transport — receive it.

The connector parses the destination prefix into a KubeMQ pattern and joins the remaining segments (slash→dot) into the channel orders.new, then bridges onto the shared KubeMQ array.

Ports & protocol surface

PortTransportProtocolNotes
61613Plain TCPSTOMP 1.0 / 1.1 / 1.2Default plain listener; binds all interfaces.
61614TLS over TCPSTOMP 1.0 / 1.1 / 1.2Binds only when the server-wide Security block resolves to TLS. Must differ from the plain port.

There is no STOMP-over-WebSocket listener — the connector speaks raw TCP only, so a WebSocket-only client (such as @stomp/stompjs) cannot drive it. TLS has no STOMP-specific configuration: certificate material, mTLS, and the minimum TLS version come from the server-wide Security block. Connecting over TLS is purely a transport swap (tls://host:61614); the STOMP frames on top are identical. See Architecture for the protocol stack.

Send a message

The example below produces one message to a Queue over a stock STOMP client. The /queue/ prefix selects the Queues pattern (competing consumer, at-least-once); the remaining segments become the KubeMQ channel with / translated to ./queue/orders/new lands on channel orders.new. Every client reads the broker endpoint from KUBEMQ_STOMP_URL (default tcp://localhost:61613); the scheme selects the transport.

package main

import (
	"fmt"
	"log"
	"os"

	"github.com/go-stomp/stomp/v3"
)

func stompURL() string {
	if v := os.Getenv("KUBEMQ_STOMP_URL"); v != "" {
		return v
	}
	return "tcp://localhost:61613"
}

func main() {
	// The scheme in KUBEMQ_STOMP_URL selects the transport; strip it for net.Dial.
	addr := stompURL()[len("tcp://"):]
	const destination = "/queue/orders/new" // Queues pattern, channel "orders.new"

	// CONNECT: accept-version 1.2, default heart-beat, no-auth.
	conn, err := stomp.Dial("tcp", addr,
		stomp.ConnOpt.AcceptVersion(stomp.V12),
		stomp.ConnOpt.Login("my-app", ""))
	if err != nil {
		log.Fatalf("connect: %v", err)
	}
	defer conn.Disconnect() //nolint:errcheck

	// SEND one message; the receipt blocks until KubeMQ accepts the frame.
	if err := conn.Send(destination, "text/plain",
		[]byte("hello from STOMP"), stomp.SendOpt.Receipt); err != nil {
		log.Fatalf("send: %v", err)
	}
	fmt.Printf("sent 1 message to %s (channel orders.new)\n", destination)
}
import os

import stomp


def stomp_endpoint() -> tuple[str, int]:
    url = os.environ.get("KUBEMQ_STOMP_URL", "tcp://localhost:61613")
    host_port = url.split("://", 1)[1]
    host, port = host_port.split(":", 1)
    return host, int(port)


def main() -> None:
    destination = "/queue/orders/new"  # Queues pattern, channel "orders.new"
    host, port = stomp_endpoint()

    # CONNECT: accept-version 1.2, default heart-beat, no-auth.
    conn = stomp.Connection([(host, port)], heartbeats=(10000, 10000))
    conn.connect(login="my-app", passcode="", wait=True)
    try:
        conn.send(destination=destination, body="hello from STOMP",
                  content_type="text/plain")
        print(f"sent 1 message to {destination} (channel orders.new)")
    finally:
        conn.disconnect()


if __name__ == "__main__":
    main()
import java.net.URI;

import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.messaging.simp.stomp.ReactorNettyTcpStompClient;

public final class Main {
    public static void main(String[] args) throws Exception {
        String url = System.getenv().getOrDefault("KUBEMQ_STOMP_URL", "tcp://localhost:61613");
        URI uri = URI.create(url);
        String destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"

        // CONNECT over raw TCP; Spring negotiates STOMP 1.2 by default.
        ReactorNettyTcpStompClient client =
                new ReactorNettyTcpStompClient(uri.getHost(), uri.getPort());
        StompSession session =
                client.connect(new StompSessionHandlerAdapter() {}).get();

        StompHeaders headers = new StompHeaders();
        headers.setDestination(destination);
        headers.add("content-type", "text/plain");
        session.send(headers, "hello from STOMP".getBytes());
        System.out.printf("sent 1 message to %s (channel orders.new)%n", destination);

        session.disconnect();
        client.shutdown();
    }
}
import { connect, type Client } from "stompit";

function endpoint(): { host: string; port: number } {
  const url = new URL(process.env["KUBEMQ_STOMP_URL"] ?? "tcp://localhost:61613");
  return { host: url.hostname, port: Number(url.port) || 61613 };
}

async function main(): Promise<void> {
  const destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"
  const { host, port } = endpoint();

  // CONNECT over raw TCP — stompit, NOT @stomp/stompjs (which is WebSocket-only).
  const client: Client = await new Promise((resolve, reject) => {
    connect({ host, port, connectHeaders: { "accept-version": "1.2",
      "heart-beat": "10000,10000", login: "my-app" } },
      (err, c) => (err ? reject(err) : resolve(c)));
  });

  const frame = client.send({ destination, "content-type": "text/plain" });
  frame.write("hello from STOMP");
  frame.end();
  console.log(`sent 1 message to ${destination} (channel orders.new)`);

  client.disconnect();
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
using System.Text;
using Stomp.Net;

static string BrokerUri()
{
    var url = Environment.GetEnvironmentVariable("KUBEMQ_STOMP_URL") ?? "tcp://localhost:61613";
    var u = new Uri(url);
    // Stomp.Net dispatches on the outer scheme: tcp:// or ssl://.
    var transport = u.Scheme == "tls" ? "ssl" : "tcp";
    return $"{transport}://{u.Host}:{(u.Port > 0 ? u.Port : 61613)}";
}

const string destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"

// CONNECT: Stomp.Net performs the STOMP 1.2 handshake; no-auth default.
var factory = new ConnectionFactory(BrokerUri(), new StompConnectionSettings());
using var connection = factory.CreateConnection();
connection.Start();
using var session = connection.CreateSession(AcknowledgementMode.AutoAcknowledge);
using var producer = session.CreateProducer(session.GetQueue(destination));

var message = session.CreateBytesMessage(Encoding.UTF8.GetBytes("hello from STOMP"));
message.StompType = "text/plain";
producer.Send(message);
Console.WriteLine($"sent 1 message to {destination} (channel orders.new)");
require "stomp"
require "uri"

uri = URI.parse(ENV.fetch("KUBEMQ_STOMP_URL", "tcp://localhost:61613"))
destination = "/queue/orders/new" # Queues pattern, channel "orders.new"

# CONNECT: accept-version 1.2, default heart-beat, no-auth.
client = Stomp::Client.new(
  hosts: [{ host: uri.host, port: uri.port }],
  connect_headers: { "accept-version" => "1.2", "heart-beat" => "10000,10000",
                     "login" => "my-app", "passcode" => "" },
)

client.publish(destination, "hello from STOMP", { "content-type" => "text/plain" })
puts "sent 1 message to #{destination} (channel orders.new)"
client.close
use async_stomp::client::Connector;
use async_stomp::ToServer;
use futures::SinkExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let url = std::env::var("KUBEMQ_STOMP_URL").unwrap_or_else(|_| "tcp://localhost:61613".into());
    let host_port = url.split("://").nth(1).unwrap_or("localhost:61613");
    let destination = "/queue/orders/new"; // Queues pattern, channel "orders.new"

    // CONNECT over raw TCP; async-stomp negotiates STOMP 1.2.
    let mut conn = Connector::builder()
        .server(host_port)
        .login("my-app".to_string())
        .passcode(String::new())
        .connect()
        .await?;

    conn.send(ToServer::Send {
        destination: destination.to_string(),
        transaction: None,
        headers: Some(vec![("content-type".to_string(), "text/plain".to_string())]),
        body: Some(b"hello from STOMP".to_vec()),
    })
    .await?;
    println!("sent 1 message to {destination} (channel orders.new)");

    conn.send(ToServer::Disconnect { receipt: None }).await?;
    Ok(())
}

Supported languages

The connector speaks standard STOMP over raw TCP, so any conformant native STOMP client works. The examples pin one client per language — there is no KubeMQ SDK, no proto bindings, and no published package. Only go-stomp/v3 and stomp.py are proven by kubemq-server integration tests; the others are wire-compatible.

LanguageClient libraryNotes
Gogithub.com/go-stomp/stomp/v3The connector's reference client.
Pythonstomp.pyListener-based; install via uv.
JavaSpring ReactorNettyTcpStompClient (spring-messaging)Raw-TCP STOMP over Reactor Netty.
JavaScript / TypeScriptstompitRaw TCP — not @stomp/stompjs (WebSocket-only).
C# / .NETStomp.NetNMS-style API over STOMP 1.2.
Rubystomp gemNative STOMP 1.0/1.1/1.2 client.
Rustasync-stompasync/await on Tokio.

@stomp/stompjs is WebSocket-only and cannot drive the STOMP connector. The connector listens on raw TCP (61613/61614) with no WebSocket upgrade, so the JavaScript/TypeScript examples use stompit (raw TCP). See Connectivity and security.

Next steps

Was this page helpful?

On this page