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.
- 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
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
}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.confStep 2 — registers publish sales, exactly like any pub/sub
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
<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:
<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:
const kv = await js.views.kv("INVENTORY_COUNTS");
const entry = await kv.get("sku-4471");
// re-render from entry.value, THEN resume the live subscriptionTroubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Browser console: WebSocket connection to 'ws://localhost:8080/' failed | websocket {} 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/wakes | Connection silently reconnected but the UI didn't re-sync current state, only resumed live updates | Always 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 moment | Expected transient — each tab connects independently and may reconnect at slightly different times | Not 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 lag | Rendering every single event directly to the DOM without batching | Debounce/batch UI updates (e.g. render at most every 250ms) instead of a DOM write per message for very high-frequency subjects |
Recap
| Concept | Takeaway |
|---|---|
websocket {} server config | The only server-side change needed — browsers become first-class NATS clients |
nats.ws | No backend relay needed; the browser subscribes directly over WebSocket |
| Reconnect handling | Must be explicit in the UI — don't let a silent reconnect imply "no data was missed" |
| KV re-sync on reconnect | Fills the "what happened while I was disconnected" gap plain pub/sub can't answer |