# STOMP (/connectors/stomp)



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 [#what-is-the-stomp-connector]

[STOMP](https://stomp.github.io/) (Simple Text Oriented Messaging Protocol) is a frame-based
text protocol: a client opens a connection with a `CONNECT` frame, then `SEND`s to and
`SUBSCRIBE`s 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 `SEND`s 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 [#how-it-works]

A STOMP client connects to the connector and `SEND`s 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.

<Mermaid
  chart="`
graph LR
APP[&#x22;STOMP client<br/>(any language)&#x22;]
CONN[&#x22;STOMP connector<br/>:61613 / :61614&#x22;]
BROKER[&#x22;Message Broker&#x22;]
SUB[&#x22;Consumer<br/>(STOMP / gRPC / REST)&#x22;]

APP -- &#x22;SEND /topic/orders/new&#x22; --> CONN
CONN -- &#x22;(events, orders.new)&#x22; --> BROKER
BROKER -. &#x22;deliver&#x22; .-> SUB

class APP,SUB client
class CONN connector
class BROKER broker
`"
/>

*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 [#ports--protocol-surface]

| Port    | Transport    | Protocol              | Notes                                                                                                       |
| ------- | ------------ | --------------------- | ----------------------------------------------------------------------------------------------------------- |
| `61613` | Plain TCP    | STOMP 1.0 / 1.1 / 1.2 | Default plain listener; binds all interfaces.                                                               |
| `61614` | TLS over TCP | STOMP 1.0 / 1.1 / 1.2 | Binds only when the server-wide `Security` block resolves to TLS. &#x2A;*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](/connectors/stomp/concepts/architecture) for the protocol stack.

## Send a message [#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.

<Tabs groupId="language" items="['Go','Python','Java','JavaScript','C#','Ruby','Rust']">
  <Tab value="Go">
    ```go
    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)
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    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()
    ```
  </Tab>

  <Tab value="Java">
    ```java
    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();
        }
    }
    ```
  </Tab>

  <Tab value="JavaScript">
    ```typescript
    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);
    });
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    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)");
    ```
  </Tab>

  <Tab value="Ruby">
    ```ruby
    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
    ```
  </Tab>

  <Tab value="Rust">
    ```rust
    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(())
    }
    ```
  </Tab>
</Tabs>

## Supported languages [#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.

| Language                | Client library                                                                                                                       | Notes                                                |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| Go                      | [`github.com/go-stomp/stomp/v3`](https://github.com/go-stomp/stomp)                                                                  | The connector's reference client.                    |
| Python                  | [`stomp.py`](https://github.com/jasonrbriggs/stomp.py)                                                                               | Listener-based; install via `uv`.                    |
| Java                    | Spring [`ReactorNettyTcpStompClient`](https://docs.spring.io/spring-framework/reference/web/websocket/stomp.html) (spring-messaging) | Raw-TCP STOMP over Reactor Netty.                    |
| JavaScript / TypeScript | [`stompit`](https://github.com/gdaws/stompit)                                                                                        | Raw TCP — **not** `@stomp/stompjs` (WebSocket-only). |
| C# / .NET               | [`Stomp.Net`](https://github.com/DaveSenn/Stomp.Net)                                                                                 | NMS-style API over STOMP 1.2.                        |
| Ruby                    | [`stomp`](https://github.com/stompgem/stomp) gem                                                                                     | Native STOMP 1.0/1.1/1.2 client.                     |
| Rust                    | [`async-stomp`](https://docs.rs/async-stomp)                                                                                         | async/await on Tokio.                                |

<Callout type="warn">
  **`@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](/connectors/stomp/how-to/connectivity-and-security).
</Callout>

## Next steps [#next-steps]

<Cards>
  <Card title="Getting started" href="/connectors/stomp/tutorials/getting-started" description="Connect, send, and subscribe end-to-end through the STOMP connector in minutes." />

  <Card title="Configuration" href="/connectors/stomp/concepts/configuration" description="The 11 connector settings, the CONNECTORS_STOMP_ENABLE disable var, ports, and validation." />

  <Card title="Events" href="/connectors/stomp/how-to/events" description="Fan-out pub/sub over STOMP — wildcard subscriptions and the slash→dot channel mapping." />

  <Card title="Destination grammar" href="/connectors/stomp/reference/destination-grammar" description="The full destination grammar, prefix→pattern table, aliases, and wildcard rules." />
</Cards>
