NATS Subject Structure
Microbus carries every microservice-to-microservice HTTP request and response over NATS. The NATS subject is the addressing string the broker uses to route the message. The layout of the subject is load-bearing for Microbus’s security model: NATS PUB/SUB ACLs are written against it, and the sender’s identity is extracted from it.
Request Subjects
A request subject has eight or more segments:
<plane>.<trust>.<port>.<src>.<dest>.<id_or_locality>.<method>.<path...>| Segment | Source | Wildcarded in subscriptions? |
|---|---|---|
plane | Tenant/test isolation prefix | No |
trust | safe for non-:666, danger for :666, reply for replies | No |
port | Numeric port the endpoint listens on, or _ when irrelevant | * when port is 0 |
src | Caller hostname, flattened (. → _, other specials %xx) | Always * |
dest | Callee hostname, flattened (. → _, other specials %xx) | No |
id_or_locality | id-XXXX, loc-XXX-YYY, or _ | No |
method | Uppercase HTTP method | * when method is ANY |
path... | One segment per path part, percent-encoded | * for path args, > for greedy path args |
The path is variable-length: each /-separated path segment becomes one NATS segment. An empty path becomes the single segment _.
Plane
plane is an opaque prefix matching [0-9a-zA-Z]* that isolates one Microbus app’s traffic from another’s on the same NATS cluster. Three sources, in priority order:
MICROBUS_PLANEenv var orconnector.SetPlane(...). Used in production to multi-tenant a shared NATS cluster.- Under
go testwith no plane set, the plane is derived from the test name so concurrent test runs do not leak messages into each other. - Default
microbusfor local development.
Trust
trust collapses two access tiers into a single segment:
safe: every port other than:666.danger: port:666, the trust-root tier (token mint, shell exec, privileged writes).reply: used only on response subjects (described below).
The trust segment exists so “any port except :666” can be expressed as one ACL pattern (<plane>.safe.*.<from>.>) without colliding with the trust-root tier. Without it, NATS deny-precedence semantics make the carve-out impossible.
Source and Destination Hostnames
Hostnames are flattened to a single NATS segment in two steps. First, every . is replaced with _ so the hostname doesn’t collide with the NATS segment separator. Then any URL-special character that a route hostname is allowed to carry ($, !, ~, etc.) is percent-encoded as %xx (2-digit lowercase hex per byte). So payments.core becomes payments_core, www.example.com becomes www_example_com, and a route hostname my$.xml becomes my%24_xml.
Service identity hostnames pass strict validation that forbids URL specials and the underscore, so for typical traffic the destination segment contains only [a-z0-9_-] and matches the legacy form one-to-one. Route hostnames (registered via absolute //some.host/path subscriptions) are the only place %xx appears in a hostname segment.
The source segment is always * in subscriptions. A service does not discriminate at the SUB layer over which peers may call it, since that is the publisher’s PUB ACL’s responsibility. Per-source SUB would also explode the subscription count by allowed-callers.
In production, NATS PUB ACLs pin each connected microservice to publish only under its own <src> segment. The receiving connector parses the source segment off the inbound message and unconditionally overwrites the publisher-set Microbus-From-Host header with that ACL-verified value before any handler runs. A malformed subject yields an empty source, which the framework treats as an unverifiable inbound message and rejects.
id_or_locality Segment
A dedicated single-segment, distinct from the destination, that carries one of three values:
id-XXXX: a specific replica’s instance ID. Publishers use this to pin a fragment stream to the replica that acked fragment 1, to address a specific replica directly, or to route a reply back to the replica that made the original request.loc-XXX-YYY: a locality identifier, broadest-first and hyphen-joined (the AWS region/AZ shape). For example, localityus-west-b-1registers slotsloc-us,loc-us-west,loc-us-west-b,loc-us-west-b-1. The publisher picks which slot to address based on the locality returned in prior response headers, narrowing toward the most specific slot whose subscribers actually answer; NATS only queue-group-dispatches within the chosen slot._: placeholder when neither prefix applies (the bare position).
Both prefixes are reserved. The framework rejects service hostnames whose first segment matches ^id- or ^loc- as well as absolute subscription routes (//host/...) that use the same prefixes. Publishers parse the URL hostname’s first segment and lift any id- or loc- prefix into the id/locality segment, leaving the remainder as the destination segment.
Method
Uppercased before emission. The literal string ANY is replaced with * in subscriptions to match every HTTP method. Unknown method tokens are rejected at registration time with 405 Method Not Allowed.
Path
Each /-separated segment of the route becomes one NATS segment. Encoding rules per segment:
| Input | Output |
|---|---|
| empty path | single _ segment |
empty segment (//) | _ |
{name} or bare * (subscription only) | * (NATS wildcard, matches any segment) |
{name...} (subscription only, last segment) | > (greedy, must be last) |
[A-Za-z0-9-] | passed through as-is |
anything else (including ., _, {, }, URL specials, multi-byte UTF-8) | %xx per byte (2-digit lowercase hex, matching url.PathEscape’s casing) |
Path encoding is case-sensitive. Uppercase letters are passed through. Hostnames, methods, and the id/locality segment are lowercased; path segments are not.
In a published request, the path goes through the general escape rule: {foo} becomes %7bfoo%7d and a literal * segment becomes %2a, because publishers send concrete path values rather than patterns. The subscription wildcard exception only applies when registering a subscription, where {foo} and bare * are equivalent ways to write “any segment in this position”.
Note that path encoding is uniform %xx for every non-alphanumeric byte (including . and _), while hostname encoding keeps the legacy . → _ substitution. The two segments live at different positions in the subject and are extracted independently, so the asymmetric encoding is unambiguous.
Reply Subjects
Replies use a fixed six-segment layout:
<plane>.reply._.<src>.<dest>.<id>The reply trust segment alone identifies the channel; the port slot uses the _ placeholder to keep the subject at constant depth. Every connector subscribes once at startup to its own response subject with the source segment wildcarded:
<plane>.reply._.*.<own_dest>.<id>This is the first subscription the connector activates, before OnStartup runs, because the connector itself makes outbound requests during startup (e.g. to configurator.core) and replies need somewhere to land.
Wildcards Summary
NATS supports two wildcards: * matches a single segment, > matches all remaining segments (must be last). Microbus uses both:
| Position | Subscription form | Reason |
|---|---|---|
| Source segment | * | Subscriptions don’t discriminate over caller identity. |
Port (when route is :0) | * | Wildcard port, by convention nothing publishes to 0. |
Method (when ANY) | * | Endpoint accepts every HTTP method. |
Path argument {name} | * | Single-segment path arg. |
Greedy arg {name...} | > | Captures the path tail; must be the last route segment. |
| Reply source | * | Replies arrive from whichever destination handled the request. |
Worked Examples
Published Subjects
| Scenario | Subject |
|---|---|
GET /PATH/to/file.html from by.com to example.com:80 | microbus.safe.80.by_com.example_com._.GET.PATH.to.file%2ehtml |
Root path GET / from by.com to www.example.com:443 | microbus.safe.443.by_com.www_example_com._.GET._ |
Trust-root POST /mint on :666 | microbus.danger.666.by_com.example_com._.POST.mint |
Per-instance pin to replica id-abcd1234 | microbus.safe.443.by_com.example_com.id-abcd1234.GET.path |
Locality-aware call to loc-us-west.example.com | microbus.safe.443.by_com.example_com.loc-us-west.GET.path |
Reply to by.com from example.com instance id-1234 | microbus.reply._.by_com.example_com.id-1234 |
Route hostname with URL special: my$.xml | microbus.safe.443.by_com.my%24_xml._.GET.path |
Subscription Subjects
| Scenario | Subject |
|---|---|
Subscription for GET /PATH/to/file.html on :80 | microbus.safe.80.*.example_com._.GET.PATH.to.file%2ehtml |
Greedy route POST /DIR/{file...} on :123 | microbus.safe.123.*.example_com._.POST.DIR.> |
Wildcard port :0, method ANY, route /foo/{f}/bar/{b} | microbus.safe.*.*.example_com._.*.foo.*.bar.* |
Reply subscription owned by example.com instance id-1234 | microbus.reply._.*.example_com.id-1234 |
ACL Implications
ACLs are derived from source code at deploy time by gencreds, which scans each microservice’s code for the call patterns that produce traffic on the bus and emits one signed <hostname>_nats.creds file per microservice. Each microservice’s .creds carries an allow-list of the exact subject patterns the code actually publishes to and subscribes from, capability-style rather than coarsely permissive.
Every microservice emits the same six framework-implicit rules (control-plane PUB on :888, reply PUB, broad SUB on own destination, reply SUB, and broadcast control SUB on all), plus a danger-port SUB only when the microservice exposes :666 endpoints. Per-route rules layered on top come from the source-code scan: typed-client method calls produce per-call PUB rules, hooks produce per-event SUB rules, outbound triggers produce self-PUB rules.
Two patterns cover the bulk of production NATS ACL design:
- Allow a service to receive from anyone on its own port range:
SUB <plane>.safe.*.*.<own_dest>.>(the source wildcard is mandatory). - Allow a service to publish to a specific destination:
PUB <plane>.safe.*.<own_src>.<dest>.>(the source segment is the verified identity; pinned by the broker to this user’s hostname). - Trust-root carve-out:
Restrict
PUB <plane>.danger.666.<own_src>.<dest>.>to the small named set of callers whose code actually invokes a:666endpoint. Without thesafe/dangersplit, this carve-out can’t be expressed without conflicting with the broadsafeallow rules. - Replies:
Each microservice needs
SUB <plane>.reply._.*.<own_dest>.*to receive its own response stream, andPUB <plane>.reply._.<own_src>.>to publish replies (the greedy>covers any destination and instance id).
The receiving connector relies on NATS having enforced the <src> segment before delivery. That is what makes the verified-source overwrite of Microbus-From-Host a real authentication signal rather than a self-attestation.
Credentials and the Plane
gencreds produces .creds tied to a specific plane. The same source code can be signed for multiple deployments (prod, staging, and so on) by re-running with a different --plane.
Each microservice loads its own <hostname>_nats.creds from CWD at startup, with a shared-default nats.creds as a fallback. Each microservice in a bundle opens its own NATS connection with its own narrow .creds. Plane wildcards in ACLs (*.safe.*.<src>.<dest>.>) are an anti-pattern. They break the cross-environment isolation the plane segment exists to provide.
Short-Circuit Transport
When two connectors live in the same process, requests bypass NATS entirely and dispatch through a process-global prefix trie. The trie supports the same * and > wildcards as NATS, so subjects emitted for short-circuit delivery are byte-identical to the NATS form. This is intentional: the receiving connector’s verified-source path runs uniformly across both transports, and in-process traffic gets the same observability and per-caller throttle behavior as cross-bundle traffic.
There is no broker to enforce ACLs in-process, so short-circuit’s source segment is publisher-attested rather than broker-verified. This is acceptable because short-circuit only delivers between connectors that already share an OS process boundary.