Back to Spy NATS
Spy NATS Blog/ Live In-Store Inventory Dashboards over NATS WebSocket

Live In-Store Inventory Dashboards over NATS WebSocket

A retail use case: pushing point-of-sale inventory-decrement events straight into a manager's browser dashboard over NATS WebSocket, with no polling and no backend relay service in between.

5 min read
  • nats
  • retail
  • websocket
  • browser
  • tutorial

The scenario

A retail chain's store managers want a live "units remaining" dashboard per item, updating the instant a sale happens at any register — without the usual "browser polls a REST endpoint every few seconds" approach, and without standing up a separate relay service just to bridge POS events into the browser.

Step 1 — enable WebSocket on the NATS server

nats-server.conf
port: 4222
http_port: 8222
websocket {
  port: 8080
  no_tls: true # local/dev only — see the security notes in the "Why Spy NATS" article for production TLS
}
terminal 1
docker run -d --name nats-server \
  -p 4222:4222 -p 8222:8222 -p 8080:8080 \
  -v "$(pwd)/nats-server.conf:/etc/nats/nats-server.conf:ro" \
  nats:latest -c /etc/nats/nats-server.conf

Step 2 — registers publish sales, exactly like any pub/sub

terminal 2
nats context add local --server="nats://localhost:4222" --select
nats pub inventory.sku-4471.sold '{"qty":1,"remaining":42}'

Step 3 — the dashboard connects directly, no backend relay

store-dashboard.html — minimal example, no build step needed
<script type="module">
  import { connect } from "https://esm.sh/nats.ws";
 
  const nc = await connect({ servers: "ws://localhost:8080" });
  const sub = nc.subscribe("inventory.*.sold");
 
  (async () => {
    for await (const msg of sub) {
      const sku = msg.subject.split(".")[1];
      const { remaining } = JSON.parse(new TextDecoder().decode(msg.data));
      document.getElementById(`sku-${sku}`).textContent = `${remaining} left`;
    }
  })();
</script>

The browser is a first-class NATS client here — there's no Node.js/Express service relaying WebSocket frames to NATS TCP frames; nats.ws speaks NATS's WebSocket protocol directly to the server.

Step 4 — reconnect handling: the dashboard must not silently go stale

Browser tabs lose network connectivity (laptop sleep, wifi drop) more often than a backend service does. nats.ws reconnects automatically by default, but the UI should reflect connection state explicitly rather than silently showing stale numbers as if they were live:

store-dashboard.html — connection status handling
<script type="module">
  import { connect } from "https://esm.sh/nats.ws";
 
  const nc = await connect({
    servers: "ws://localhost:8080",
    reconnect: true,
    maxReconnectAttempts: -1, // retry indefinitely
  });
 
  (async () => {
    for await (const status of nc.status()) {
      const el = document.getElementById("connection-status");
      if (status.type === "disconnect") el.textContent = "⚠️ Reconnecting…";
      if (status.type === "reconnect") el.textContent = "✅ Live";
    }
  })();
</script>

Step 5 — filling the gap after a reconnect

Plain pub/sub tells the dashboard nothing about sales that happened while it was disconnected. Pair the live subject with a small KV bucket holding each SKU's current count (same pattern as the device-status and gaming-leaderboard articles), so a reconnecting dashboard re-reads current truth instead of trusting whatever number it last had:

on reconnect — re-sync current counts, then resume watching
const kv = await js.views.kv("INVENTORY_COUNTS");
const entry = await kv.get("sku-4471");
// re-render from entry.value, THEN resume the live subscription

Troubleshooting

SymptomLikely causeFix
Browser console: WebSocket connection to 'ws://localhost:8080/' failedwebsocket {} block missing, or the dashboard page is served over https:// (mixed content blocks ws://)Confirm the server logs show "Listening for websocket clients", and use wss:// with a real TLS cert when the page itself is HTTPS
Dashboard shows stale numbers after a laptop sleeps/wakesConnection silently reconnected but the UI didn't re-sync current state, only resumed live updatesAlways re-fetch current truth (e.g. from a KV bucket) on reconnect, don't assume no sales happened while disconnected
Multiple dashboard tabs all update, but inconsistently for a momentExpected transient — each tab connects independently and may reconnect at slightly different timesNot a bug — if strict consistency across tabs matters, have the dashboard also re-sync from KV on every reconnect, as in Step 5
High register volume causes the browser tab to visibly lagRendering every single event directly to the DOM without batchingDebounce/batch UI updates (e.g. render at most every 250ms) instead of a DOM write per message for very high-frequency subjects

Recap

ConceptTakeaway
websocket {} server configThe only server-side change needed — browsers become first-class NATS clients
nats.wsNo backend relay needed; the browser subscribes directly over WebSocket
Reconnect handlingMust be explicit in the UI — don't let a silent reconnect imply "no data was missed"
KV re-sync on reconnectFills the "what happened while I was disconnected" gap plain pub/sub can't answer

SpyNATS is an independent tool and is not affiliated with, sponsored by, or endorsed by Synadia Communications or the NATS.io project.