NATS Goes Outside

May 17, 2026

For most of the last decade, the rule for high-throughput message brokers was simple: keep them inside the VPC, behind a REST gateway, and let backend services talk to them on the trusted network. Exposing one directly to a browser tab, a phone, or a fleet of cell-connected sensors meant building a stateful translation layer in front and paying the latency cost. NATS removes the rule.

What changes is not the messaging model β€” that survives the trip outside. What changes is the discipline you bring to three things: how external clients reach the cluster, how the broker decides who they are, and what each one is allowed to touch. The rest of this post is those three things.

Topology

Before identity, there is transport. A modern browser cannot open a raw TCP socket. A mobile app inside a restrictive corporate network often cannot either. The traditional fix β€” an NGINX with Lua plugins, a Node bridge, a REST gateway faking async β€” added latency, broke end-to-end backpressure, and turned the gateway into its own stateful thing to scale and secure.

NATS speaks WebSocket natively at the broker. Browsers, mobile apps, and IoT devices connect on the same port your firewall is already letting through (typically 443) and use the same publish, subscribe, and request/reply primitives that backend services use. No translation layer. The broker is the edge.

Pencil schematic of the NATS WebSocket edge architecture. On the left, three external clients stacked vertically β€” a browser, a mobile app, an IoT device β€” each connecting via a labeled 'wss :443' arrow to a central NATS Server box marked 'native WebSocket support'. On the right, three backend services connect to the same NATS Server via labeled 'tcp :4222' arrows. A caption below the central server reads 'no REST gateway Β· no protocol proxy Β· no translation layer'.
External clients and backend services connect to the same broker over different transports. No proxy in the middle.

For anything beyond a single broker, NATS scales through a layered topology. A cluster of three to five servers in one region holds full-mesh peer connections and gossips about who has joined. A super-cluster joins multiple regional clusters through gateway links, so a publisher in one region can reach a subscriber in another. And leaf nodes sit on the far edge β€” at a customer site, a factory floor, an IoT cellular hub β€” proxying local clients to an upstream cluster over a single outbound TLS connection.

Pencil schematic of a NATS super-cluster spanning two regions, drawn left to right. On the far left, four external client types stacked vertically: a browser, a mobile app, an IoT fleet thermostat, and a stick-figure B2B partner. Each client has an arrow going right to one of two leaf nodes in a middle column. Each leaf node has an outbound TLS arrow on port 7422 reaching into Regional Cluster A in the center β€” a triangle of three full-mesh-connected servers labeled nats-1, nats-2, nats-3. A labeled GATEWAY link with an encryption symbol connects Regional Cluster A to Regional Cluster B on the right, which has its own three-server full mesh of nats-4, nats-5, nats-6. A caption at the bottom reads: 'External clients connect via leaf nodes Β· regional clusters use full-mesh + gossip Β· gateways federate clusters into a super-cluster'.
External clients on the left, leaf-node trust hops in the middle, two full-mesh regional clusters joined by a gateway on the right.

The leaf node is the load-bearing piece for external-facing deployments. It is not part of the cluster mesh; it is a bridge attached to it. Many untrusted local clients collapse into one explicit, account-scoped trust hop into the cluster. A browser connecting to wss://app.example.com may land on a leaf, on a server in Region A, or on a server in Region B; gossip and interest propagation mean the same messaging fabric appears regardless of which physical endpoint accepted the connection. From any single client's perspective there is just one broker β€” and that is the point.


Identity

Once a client can connect, the next question is who they are. The wrong answer is a central user database that every broker queries β€” it bottlenecks, it is a juicy target, and it puts the broker in the identity-management business. NATS picks a different answer: identity is a delegated chain.

Three tiers. An operator is the root of trust; its public key is baked into the server config. The operator signs account credentials β€” one per logical tenant. Each account, in turn, signs user credentials β€” one per connection. Every client carries its user credential and presents it at connect time. The broker verifies the signature chain back to the operator. The broker never stores user records and never sees a private key. A breach of the broker reveals nothing useful.

Pencil schematic of the NATS JWT trust hierarchy on a warm cream parchment background, drawn landscape and reading left to right. Three labeled rectangular boxes are arranged horizontally. Left box: OPERATOR NKey, with the subtitle 'root of trust Β· server config Β· kept offline'. A horizontal arrow labeled 'signs Account JWTs' connects it to the middle box. Middle box: ACCOUNT NKey, with subtitle 'tenant boundary Β· isolated subject namespace'. A second horizontal arrow labeled 'signs User JWTs' connects it to the right box. Right box: USER JWT, with subtitle 'per-connection identity Β· presented at CONNECT'. Below the right box, a small simple laptop icon labeled 'CLIENT' has a dashed arrow pointing up to the USER JWT box, labeled 'presents at connect'.
Each tier signs the one below. The client never holds anything above its own user credential.

This is decentralization that matters in practice. A platform team can hand an account to an external organization β€” a partner, a customer, a regional subsidiary β€” and that organization issues credentials for its own users without coordinating with the platform team, without modifying server config, and without sharing private keys. The operator's signature on the account credential is all the broker needs.

The chain works when you can adopt the broker's identity workflow. Most organizations cannot. They have Okta, LDAP, a users table with bcrypt hashes, an OAuth provider β€” and they want NATS connections to authenticate against whatever they already run.

Auth Callout is the answer. When a client connects, the broker doesn't decide on its own. It forwards the connection attempt to an external auth service β€” a small microservice the team writes themselves β€” which makes the real decision against whatever identity backend it likes. The service replies with a freshly-minted user credential, and the broker binds that to the connection for its lifetime. Sensitive material on the wire (plain-text passwords from legacy clients, bearer tokens) is encrypted with one-time ephemeral keys so even an attacker with access to the internal messaging plane learns nothing useful.

Auth Callout flow across three actors with XKey encryption A three-lane vertical sequence diagram. CLIENT lane on the left, NATS SERVER lane in the middle, AUTH SERVICE lane on the right. Time flows top to bottom. Step 1: the client opens a connection to the NATS server presenting raw credentials. Step 2: the server constructs an Authorization Request Claim, encrypts it with a one-time XKey, and publishes the encrypted request on the dollar-sign-sys.req.user.auth subject to the auth service. Step 3: the auth service decrypts the request with its XKey, then queries an external identity provider (LDAP, SQL, or OAuth) to validate the credentials. Step 4: the service mints a scoped User JWT, wraps it in an Authorization Response Claim, encrypts the response with the server's one-time public XKey, and publishes back. Step 5: the server decrypts the response, binds the User JWT's permissions to the connection, and discards the one-time XKey. CLIENT NATS SERVER AUTH SERVICE CONNECT + raw credentials (user/pass, bearer token, etc.) publish on $SYS.REQ.USER.AUTH [ encrypted with one-time XKey ] decrypt with XKey query IdP / DB validate creds scoped User JWT (Authorization Response Claim) [ encrypted with server's XKey ] OK β€” bound, XKey discarded
Three actors, five messages. The broker doesn't decide who you are β€” it asks the auth service and binds whatever the service replies.

The payoff is that existing identity systems get a NATS face without being rebuilt inside NATS. Service identities that never change connection-to-connection can stay on long-lived credentials and skip the round trip entirely. Auth Callout earns its place wherever identity is dynamic or the backend of record lives somewhere else.


Access Scope

A connected client has a verified identity. The next question is what it can touch. NATS answers in subjects β€” hierarchical strings like orders.us-east.45 that double as both the addressing primitive and the access-control primitive. Every credential carries allow and deny lists of subject patterns; the broker checks every publish and subscribe against them.

Two engine rules make the system predictable. Deny wins when a subject matches both an allow and a deny rule β€” useful for granting a broad prefix to a device while locking out a sensitive sub-hierarchy in the same breath. And anything not explicitly allowed is denied: once you list one subject in an allow list, you have implicitly denied every other. You cannot accidentally widen a permission by forgetting to write one down.

Pencil schematic of a subject authorization decision tree. At the top, a rules box headed 'USER PERMISSIONS' lists two rules: 'pub allow: telemetry.deviceA.>' and 'pub deny: telemetry.deviceA.admin.>'. Below, four publish attempts each show the subject on the left and an outcome on the right. Attempt 1 (telemetry.deviceA.temp): ALLOWED β€” matches allow rule. Attempt 2 (telemetry.deviceA.humidity): ALLOWED β€” matches allow rule. Attempt 3 (telemetry.deviceA.admin.reboot): DENIED β€” deny precedence. Attempt 4 (telemetry.deviceB.temp): DENIED β€” default deny, no allow matches.
Four publish attempts against the same two-rule permission set. Allow, deny precedence, and default deny all visible at a glance.

The reason this scales to thousands of users without thousands of policy blocks is per-identity templating. A single permission rule can carry the user's own identity as a variable β€” "publish to orders.<your-user-id> and nothing else" β€” so every credential issued under that account naturally scopes itself to its own slice of the subject space. You write one rule; the broker applies it differently for every connection.

Tenant bridges. Subject permissions scope an individual user. They do not scope an entire tenant. For SaaS platforms putting many customers on the same broker, the right primitive is the account itself β€” the same account that sits as the middle tier of the identity chain. Each account is a sealed subject namespace: a publish from account A to orders and a subscribe in account B to the same string see nothing of each other. Cross-tenant data leak is not policy-enforced; it is architecturally impossible.

Real systems still need deliberate sharing β€” a control plane fanning out to every customer, a partner account exposing one feed to a downstream account, a multi-region deployment moving streams between regions. NATS handles this through exports and imports: an explicit, named, asymmetric bridge between otherwise-sealed accounts.

Loose pencil-sketch illustration of two boxy buildings side by side on a warm cream parchment background, labeled in large hand-lettered ALL-CAPS at the top: ACCOUNT A on the left, ACCOUNT C on the right. Account A's wall icons show laptops and database cylinders; Account C's wall icons show dashboard graphs and document pages. A thick crosshatch-shaded wall stands between the two buildings. At ground level, a small enclosed walkway crosses through the wall with a hand-lettered sign reading 'EXPORT β†’ IMPORT'; inside the walkway, a small package labeled 'from_a.>' is being carried from Account A toward Account C with an arrow indicating direction.
Two tenant buildings, one wall, one explicitly named skybridge. The wall is the default; the bridge is the contract.
Cross-account subject mapping via Exports and Imports Two rounded rectangles side by side, labelled Account A on the left and Account C on the right. Inside Account A: a subject node holds the deeply nested name 'system.core.v2.telemetry.device.metrics.>'. An arrow leaves Account A, crosses the gap labelled 'Export with restricted accounts allowlist', and enters Account C. Inside Account C: a much shorter subject node labelled 'from_a.>' represents the locally mapped import. Beneath it sits an external client labelled 'subscribes to from_a.>' β€” unaware of the full source path or that the data crosses an account boundary. Hover or focus any element to reveal what it represents. system.core.v2.…metrics.> subject in Account A's namespace exported with accounts allowlist from_a.> subscribes from_a.>
Hover or tab through each element. Account A's deep subject becomes Account C's short local prefix; external clients in C never see the cross-account topology.

The asymmetry is the trick. The source account decides whether to expose a subject at all (publicly, or only to a named allowlist of destination accounts). The consuming account decides what local name the imported subject appears under. External clients in the consumer see a short, friendly prefix and have no idea the data lives in another tenant β€” and no permission to attempt any other subject in that tenant's namespace.


Trading Dash

The three pillars compose differently for different problems. Two worked scenarios show the shape.

A financial firm wants to push real-time equity quotes to a browser-based dashboard. The client is untrusted JavaScript on whatever laptop the user happens to own. Any credential held in browser memory has to do exactly one thing and no more.

Pencil sketch of a trader sitting at a desk facing a laptop. The laptop screen shows a stylized 'TRADING DASHBOARD' with a stock ticker reading 'AAPL up, TSLA down, NVDA up, MSFT up'. A pipe-like connection labeled 'wss :443' goes from the laptop up and to the right to a box labeled 'NATS BROKER'. On the pipe, a small ticket icon is labeled 'USER JWT' with subtitles 'sub: market.data.equities.>', 'WEBSOCKET only', '8h expiry Β· subs: 50'; a small arrow annotation reads 'enforced by cryptography'. To the right of the broker, a cylinder labeled 'MARKET DATA FEED' pumps data into the broker.
The browser holds a JWT ticket; the broker enforces every restriction on the ticket cryptographically.

Topology. The browser connects directly to the broker over WebSocket on port 443 β€” the same port the user's firewall is already letting through. No leaf node, no REST gateway, no protocol translator. The broker is the edge.

Identity. The firm's existing portal stays the identity authority. When the user logs in (with the same OIDC and MFA flow they already use), a backend service mints a fresh, short-lived NATS credential bound to that session and hands it back to the browser over the existing HTTPS connection. The browser presents that credential at connect; the broker verifies the chain back to the firm's account. There is no NATS-specific login screen and no separate user database.

Access scope. The minted credential is locked down hard. Subscribe is restricted to market.data.equities.>. Publishing is denied for every subject (a leaked credential cannot inject fake market data). The credential expires after eight hours, is bound to the WebSocket transport so it cannot be replayed over raw TCP, and caps the total number of subscriptions so the browser cannot exhaust the broker.

Trading dashboard connection lifecycle as a three-actor message flow A three-lane vertical sequence diagram showing the message flow when a trader signs in. BROWSER lane on the left, BACKEND lane in the middle, NATS BROKER lane on the right. Time flows top to bottom. Step 1: the browser sends HTTPS login credentials (with MFA) to the backend. Step 2: an internal action on the backend lane β€” the backend generates an ephemeral NKey pair for this session. Step 3: another internal action on the backend lane β€” the backend mints a restrictive User JWT scoped to market.data.equities subscribe only, with 8-hour expiry, locked to WEBSOCKET transport, and a 50-subscription cap. Step 4: the backend returns the JWT plus the private NKey seed to the browser over the existing HTTPS session. Step 5: the browser opens a WebSocket connection to the NATS broker, signs the nonce challenge locally with the seed in memory, and the broker validates the three-tier chain. Step 6: the broker binds the permissions and market data begins streaming to the browser. BROWSER BACKEND NATS BROKER HTTPS login (OIDC + MFA) generate ephemeral NKey pair mint restrictive JWT: sub: market.data.equities.> WS-only Β· 8h Β· subs:50 JWT + private NKey seed (HTTPS) wss :443 β€” JWT + signed nonce (broker walks the three-tier chain) market data stream begins (every subscription enforced by JWT permissions)
Three actors, six exchanges. The backend is the only thing that mints credentials; the browser only holds them; the broker only trusts them via cryptography.

The trust boundary is the credential, not the network.


IoT Migration

A smart-home provider has tens of thousands of legacy thermostats already deployed in customers' houses, each one authenticating with a hardcoded MQTT username and password baked in at the factory. The provider wants to move the backend to a NATS-based multi-tenant platform β€” without an over-the-air firmware update and without breaking any deployed device.

Pencil sketch of legacy IoT thermostat migration on a warm cream parchment background. On the left, three old-style dial thermostats stacked vertically, each labeled 'HARDCODED CREDS' and emitting a speech bubble reading 'MQTT username/password'. Dashed wireless signal lines go right from each thermostat, converging into a central box labeled 'NATS BROKER'. Adjacent to the broker on the right, a smiling stick-figure character labeled 'AUTH CALLOUT SERVICE' holds a JWT ticket; the service has a connecting line to a small book labeled 'SQL DATABASE'. On the far right, three vertical container silos labeled 'CUSTOMER_X', 'CUSTOMER_Y', 'CUSTOMER_Z'. An arrow from the broker routes one specific thermostat's connection into the CUSTOMER_X silo, labeled 'mapped to tenant at connect'.
The thermostat sends what it always sent. The broker, the auth service, and the SQL database do the tenant mapping invisibly.

Topology. Each thermostat speaks MQTT to a leaf node sitting at the regional edge, which proxies the connection onto the upstream cluster. Adding more leaf nodes scales the fleet linearly; the cluster itself does not change shape as the device count grows.

Identity. The thermostat cannot be modified, so it cannot generate keys or present a JWT β€” all it will ever send is its hardcoded username and password. Auth Callout bridges that. The broker forwards every connection attempt to an auth service that looks the device up in the provider's existing SQL database of device-to-customer mappings, finds which customer the device belongs to, and mints a NATS credential on the fly that places the device into that customer's account.

Access scope. The minted credential restricts publishing to that one device's own telemetry subjects (telemetry.device_beta_99.>) and nothing else. One compromised thermostat cannot publish on another device's behalf and cannot see another customer's traffic β€” the account boundary is sealed at the protocol level, not by a policy that could be miswritten.

IoT thermostat auth callout as a four-actor message flow A four-lane vertical sequence diagram showing what happens at the broker when one legacy thermostat reconnects. Lanes from left to right: THERMOSTAT (the legacy MQTT device), NATS BROKER, AUTH SERVICE (Go microservice), SQL DB (the existing customer database). Time flows top to bottom. Step 1: the thermostat opens an MQTT connection to the broker presenting its hardcoded username and password. Step 2: the broker constructs an Authorization Request Claim with the credentials, generates a one-time XKey, encrypts the payload, and publishes it to the auth service on the system auth subject. Step 3: the auth service decrypts with its XKey, then queries the SQL database for the device's tenant assignment, which returns Customer_X. Step 4: the auth service mints a User JWT with issuer_account set to Customer_X and publish permission scoped to telemetry.device_beta_99, then encrypts the response with the broker's one-time XKey and publishes back. Step 5: the broker decrypts, binds the JWT to the connection, discards the XKey, and routes the thermostat into the Customer_X account silo. THERMOSTAT NATS BROKER AUTH SERVICE SQL DB MQTT CONNECT user=device_beta_99, pass=legacy_secret $SYS.REQ.USER.AUTH (request) [ encrypted with one-time XKey ] SELECT tenant FROM devices Customer_X scoped User JWT issuer_account=Customer_X Β· pub: telemetry.device_beta_99.> [ encrypted with broker's XKey ] OK β€” routed into Customer_X silo (XKey discarded, device sees no change)
Four actors, five exchanges. The thermostat never knows the SQL database exists; the SQL database never knows the broker exists.

The device sees no change. The credentials it sends are the credentials it has always sent. Everything that makes this an external-facing multi-tenant deployment happens between the broker, the auth service, and the SQL database β€” invisible to the deployed fleet.


Putting NATS in front of external clients is not "punch a hole in the firewall." It is composing three things deliberately. Topology decides where the bridge sits β€” leaf nodes catching untrusted clients, regional clusters carrying interest, gateways federating across distance. Identity decides who gets in β€” a delegated chain the broker verifies cryptographically, with Auth Callout as the escape hatch for systems whose users already live somewhere else. Access scope decides what each one can touch β€” per-identity, default-deny, and sealed at the account boundary.

Get the three right and the inversion holds: the internal cluster trusts everything implicitly because it is safe to; the external-facing cluster trusts nothing implicitly and verifies every contract at every connect. Not "expose the cluster" β€” collapse implicit edges into explicit hops.

Pencil sketch of three classical stone pillars standing in a row, each carved with visible hatching for texture. Above the three pillars a single long horizontal stone lintel carries a hand-lettered inscription: 'VERIFIED CONTRACT β€” at every CONNECT'. The three pillars are labeled at their bases, left to right: TOPOLOGY, IDENTITY, ACCESS SCOPE. Below each base label, a smaller italic subtitle notes what each pillar provides: 'TOPOLOGY: where the bridge sits'; 'IDENTITY: who gets in'; 'ACCESS SCOPE: what they can touch'.
Three pillars holding up the same lintel. Remove any one and the contract collapses.

References

  1. NATS Docs. WebSocket transport configuration β€” listener parameters, TLS requirements, origin controls.
  2. NATS Docs. Clustering, super-clusters, and gateways β€” full-mesh routing, gossip discovery, cross-region federation.
  3. NATS Docs. Leaf nodes β€” outbound TLS, account-scoped trust hop, edge-facing topology.
  4. nats-io/nats.js β€” official WebSocket client library for browser, Node.js, and Deno. (The standalone nats.ws repo was archived May 2026 and folded into nats.js; older posts and tutorials may still reference the archived repo.)
  5. NATS Docs. Managing JWT Security β€” three-tier hierarchy, signing chain, server-side validation.
  6. NATS Docs. Decentralized JWT Authentication / Authorization β€” Ed25519 NKey identity, nonce-signing handshake.
  7. NATS Docs. Scoped Signing Keys β€” permission templates on the signing key, instant policy propagation.
  8. NATS Docs. Auth Callout β€” request/response claim schemas, XKey encryption, AUTH account convention.
  9. synadia-io/callout.go β€” Go library for building Auth Callout services.
  10. NATS Docs. Subject Authorization β€” wildcard syntax, allow/deny precedence, response permissions.
  11. NATS Docs. Multi-Tenancy using Accounts β€” namespace isolation, Exports and Imports, cross-account subject mapping.
  12. Synadia. Synadia (managed NATS, NGS) β€” managed Operator for organizations that don't want to self-host the trust root.

← Back to bootloader.live