# Destination mapping (/connectors/stomp/how-to/destination-mapping)



The STOMP `destination` is the single most important mental model in this connector. A
destination's **first segment selects the KubeMQ pattern**; the remaining segments are
**slash→dot joined** into the KubeMQ channel. This is how an unmodified STOMP application reaches all
five KubeMQ patterns by changing only the address it dials.

## The grammar [#the-grammar]

The connector resolves a destination in these steps:

1. The raw destination length is checked first — **≤ 512 bytes**, on the raw string **before** the
   leading-slash strip.
2. Strip **exactly one** leading `/` (both `/queue/x` and `queue/x` are accepted).
3. Split on `/`. Any **empty segment** (from `//`, a trailing `/`, etc.) is rejected as `invalid
   destination`.
4. The **first segment** selects the pattern via a **case-sensitive** map.
5. The remaining segments are **`.`-joined** into the KubeMQ channel.
6. An **unknown** first segment falls back to `DefaultPattern` (see below).

### Primary prefix → pattern → channel [#primary-prefix--pattern--channel]

Lead with these **primary** names in every application and example:

| STOMP destination (ingress) | Pattern                  | KubeMQ channel | Canonical egress (always primary) |
| --------------------------- | ------------------------ | -------------- | --------------------------------- |
| `/queue/orders/new`         | Queues                   | `orders.new`   | `/queue/orders/new`               |
| `/topic/a/b/c`              | Events                   | `a.b.c`        | `/topic/a/b/c`                    |
| `/topic-store/audit`        | Events-Store             | `audit`        | `/topic-store/audit`              |
| `/command/exec`             | Commands (RPC)           | `exec`         | `/command/exec`                   |
| `/query/lookup`             | Queries (RPC)            | `lookup`       | `/query/lookup`                   |
| `/reply/r1`                 | reply (connection-local) | `r1`           | `/reply/r1`                       |

<Callout type="info">
  The Events-Store prefix is &#x2A;*`/topic-store/`** (primary). There is **no** `/topic_store/` or
  `/eventstore/`.
</Callout>

## Slash → dot, and the literal-dot trap [#slash--dot-and-the-literal-dot-trap]

The channel join is **slash→dot**: `/topic/a/b/c` → channel `a.b.c`. Egress reverses it: channel
`a.b.c` → `/topic/a/b/c`. A **literal `.` inside a segment passes through unchanged** — which makes
it lossy.

<Callout type="warn">
  **A literal `.` in a destination segment is lossy.** Both `/topic/a.b` and `/topic/a/b` map to the
  **same** KubeMQ channel `a.b`, and egress **always emits the slash form** `/topic/a/b`. So
  `/topic/a.b` is **not** round-trip safe — a subscriber will see `/topic/a/b` on the MESSAGE
  `destination`. &#x2A;*Prefer slashes; avoid literal dots in destination segments.**
</Callout>

```text
SEND  /topic/a.b   ─┐
                    ├─►  channel "a.b"  ─►  MESSAGE destination /topic/a/b
SEND  /topic/a/b   ─┘
```

## MQTT-name aliases (never lead with them) [#mqtt-name-aliases-never-lead-with-them]

For migration convenience, each primary prefix has an MQTT-style alias. They route identically, but
**egress always canonicalizes to the primary name** — a subscriber never sees the alias on the
delivered MESSAGE `destination`.

| Primary (lead with this) | Alias        | Pattern                  |
| ------------------------ | ------------ | ------------------------ |
| `/queue/`                | `/queues/`   | Queues                   |
| `/topic/`                | `/events/`   | Events                   |
| `/topic-store/`          | `/store/`    | Events-Store             |
| `/command/`              | `/commands/` | Commands (RPC)           |
| `/query/`                | `/queries/`  | Queries (RPC)            |
| `/reply/`                | *(none)*     | reply (connection-local) |

The aliases exist only so an in-flight migration from MQTT-style naming keeps working; because
egress canonicalizes, a fleet that subscribes via `/events/x` and a fleet that subscribes via
`/topic/x` both receive `/topic/x` on the wire. &#x2A;*Always lead docs, examples, and application code
with the primary names.**

## DefaultPattern (bare destinations) [#defaultpattern-bare-destinations]

A destination whose first segment is **not** a known prefix is treated as **bare** and routed by the
connector's `DefaultPattern` config:

| `DefaultPattern`                      | Bare `sensor/temp` resolves to       | Channel       |
| ------------------------------------- | ------------------------------------ | ------------- |
| `events&#x60; &#x2A;(config default)* | Events                               | `sensor.temp` |
| `queues`                              | Queues                               | `sensor.temp` |
| `store`                               | Events-Store                         | `sensor.temp` |
| `none`                                | **rejected** (`invalid destination`) | —             |

There is **no** `commands` / `queries` default — a bare destination can **never** resolve to an RPC
pattern.

<Callout type="info">
  **Examples always use explicit prefixes.** Relying on `DefaultPattern` couples your application to a
  server-side config you do not control. Write `/topic/sensor/temp`, not `sensor/temp`.
</Callout>

## Wildcards — Events only, subscribe only [#wildcards--events-only-subscribe-only]

Wildcards are pass-through to the message broker's **native wildcard syntax** — the connector does
**not** translate them:

* `*` matches **one** segment, in any position.
* `>` matches the **tail**, and **must be the final** segment.

<Callout type="warn">
  **Wildcards are Events-only and SUBSCRIBE-only, and use the broker's native syntax (`*` = one
  segment, `>` = final tail).** A wildcard is allowed **only** on `SUBSCRIBE` and **only** for the
  Events pattern. A wildcard on `SEND`, or on **Queues / Events-Store / RPC**, is rejected as `invalid
  destination`. &#x2A;*There is no MQTT-style `+` / `#`.** And **egress delivers the concrete matched
  channel, not the filter** — a `/topic/orders/*` subscriber receiving on `orders.eu` gets
  `destination:/topic/orders/eu`.
</Callout>

```text
SUBSCRIBE  /topic/orders/*    → channel filter "orders.*"  (Events, OK)
SUBSCRIBE  /topic/orders/>    → channel filter "orders.>"  (> must be final, OK)
SEND       /topic/orders/*    → invalid destination
SUBSCRIBE  /queue/jobs/*      → invalid destination

# Egress delivers the concrete channel:
SUBSCRIBE  /topic/orders/*  ──►  MESSAGE destination /topic/orders/eu   (NOT /topic/orders/*)
```

Slash→dot still applies inside wildcard filters: `/topic/a/*/c` → `a.*.c`. Wildcards do **not** apply
to Events-Store.

## Header ⇄ tag interop [#header--tag-interop]

The body is byte-exact in both directions (the writer always stamps `content-length` on egress, so
it is binary-safe). **Metadata is mapped between STOMP headers and KubeMQ Tags** by a precise
convention.

### The five standard headers ↔ reserved `stomp.*` tags [#the-five-standard-headers--reserved-stomp-tags]

These five round-trip in **both** directions:

| STOMP header (ingress & egress) | KubeMQ Tag key         |
| ------------------------------- | ---------------------- |
| `content-type`                  | `stomp.content-type`   |
| `correlation-id`                | `stomp.correlation-id` |
| `reply-to`                      | `stomp.reply-to`       |
| `priority`                      | `stomp.priority`       |
| `type`                          | `stomp.type`           |

A SEND `content-type:application/json` becomes Tag `stomp.content-type=application/json`; on egress
that tag is restored to a `content-type` header.

### Custom headers pass through as tags [#custom-headers-pass-through-as-tags]

Any header **not** in the standard set and **not** protocol machinery passes through **name-as-is**
as a KubeMQ tag. A custom `x-trace:abc` on SEND becomes Tag `x-trace=abc`, delivered back as an
`x-trace` header. Duplicate header names are **first-wins**.

<Callout type="warn">
  **Tag limits are fatal on SEND.** At most **32** custom tags, at most **4096 bytes** per tag value.
  Exceeding either returns `ERROR "frame too large"` and closes the connection.
</Callout>

### Collision rule, machinery headers, and the spoofing guard [#collision-rule-machinery-headers-and-the-spoofing-guard]

* **Collision rule — `stomp.*` wins.** If a native producer sets both a bare `content-type` tag and
  a `stomp.content-type` tag, the `stomp.*` tag wins on egress, and exactly one `content-type` header
  is emitted.
* **Machinery headers never become tags:** `destination`, `receipt`, `transaction`,
  `content-length`, `message-id`, `subscription`, `ack`, `id`, `timeout`.
* **Spoofing guard.** A client **cannot inject** `stomp.*` tags directly. On ingress, any header
  whose name starts with `stomp.` or equals `x-kubemq-metadata` is **silently stripped** — the only
  way to set a `stomp.*` tag is through the corresponding standard header.
* **`x-kubemq-metadata` is egress-only.** STOMP ingress never sets the KubeMQ `Metadata` field. On
  egress, if a native producer left a non-empty `Metadata`, it surfaces as a **read-only**
  `x-kubemq-metadata` header. A STOMP client can observe it but cannot write it.

<Callout type="warn">
  **There is no `content-type` frame default.** A native producer (gRPC/REST/MQTT/AMQP) that sets no
  content-type tag produces a MESSAGE with **no `content-type` header** — the STOMP subscriber must
  assume binary / octet-stream. `content-length` is always present, so the body is still framed
  correctly. To surface a proper `content-type`, a native producer should set
  `Tags["stomp.content-type"]`.
</Callout>

## Egress representability per version [#egress-representability-per-version]

Not every header value can be serialized on every negotiated STOMP version. The connector drops
unrepresentable headers on egress rather than corrupting the frame:

| Negotiated version | Drops on egress                               |
| ------------------ | --------------------------------------------- |
| **1.0**            | `:`, CR, or LF in a name; CR or LF in a value |
| **1.1**            | CR (1.1 has no escape for it)                 |
| **1.2**            | nothing — always representable                |

<Callout type="warn">
  **CR/LF in a header value is silently dropped to 1.0/1.1 subscribers.** STOMP 1.1 cannot represent a
  raw CR; the guard drops the unrepresentable header without corrupting the frame, and the connection
  stays alive. &#x2A;*Use `accept-version:1.2` and keep CR/LF out of header values; structured or multiline
  metadata belongs in the body.**
</Callout>

## Destination errors (all sanitized) [#destination-errors-all-sanitized]

| Trigger                                        | Wire `message`        |
| ---------------------------------------------- | --------------------- |
| `//`, trailing `/`, `/queue/` (empty segment)  | `invalid destination` |
| `/queue` (prefix, no channel)                  | `invalid destination` |
| Bare destination with `DefaultPattern=none`    | `invalid destination` |
| Raw destination > 512 bytes                    | `invalid destination` |
| Wildcard on SEND / Queues / Events-Store / RPC | `invalid destination` |

Internal error text never crosses the wire — every destination error surfaces as the single sanitized
`invalid destination` message.

## Related [#related]

<Cards>
  <Card title="Destination grammar" href="/connectors/stomp/reference/destination-grammar" description="The formal grammar and the full header reference tables." />

  <Card title="Protocol versions" href="/connectors/stomp/how-to/protocol-versions" description="Header escaping and the egress representability guard per version." />

  <Card title="Events" href="/connectors/stomp/how-to/events" description="Wildcard subscribe and fan-out examples in practice." />
</Cards>
