In an inventory management application, the hard part is not recording stock coming in or going out. It is making sure everyone sees the same quantity at the same moment.
Picture two people looking at the same product. One records an outgoing movement. If the other person's screen does not update, they are working with a wrong number, and may sell something that no longer exists.
That is the question I had to answer while building an inventory management application with Nuxt, TypeScript and Pinia.
Polling the server?
The first idea is polling: asking the server every few seconds whether anything changed. It is simple, but it is a poor trade-off.
- Poll too often and you multiply useless requests, since most of them bring back nothing new.
- Poll too rarely and screens stay wrong for several seconds.
WebSockets flip the logic. The browser keeps a connection open, and the server notifies clients as soon as a stock movement is recorded.
The principle
The flow is deliberately simple:
- A user records a movement through the regular API.
- The server saves it to the database, then broadcasts an event to every connected client.
- Each client applies that movement to its local state.
A Pinia store that applies movements
On the interface side, Pinia centralises the stock state. Every incoming movement goes through a single action:
import { defineStore } from "pinia";
type Movement = {
id: string;
productId: string;
quantity: number;
type: "in" | "out";
};
export const useStockStore = defineStore("stock", {
state: () => ({
quantities: {} as Record<string, number>,
appliedMovements: new Set<string>(),
}),
actions: {
applyMovement(movement: Movement) {
if (this.appliedMovements.has(movement.id)) return;
const delta = movement.type === "in" ? movement.quantity : -movement.quantity;
const current = this.quantities[movement.productId] ?? 0;
this.quantities[movement.productId] = current + delta;
this.appliedMovements.add(movement.id);
},
},
});Every component that displays a quantity reads from this store. When a movement arrives, they update on their own.
Why the movement ID matters
Notice appliedMovements. The same movement can arrive twice: the user who created it receives the API response and the event broadcast to everyone.
Without protection, their quantity would be counted twice. By ignoring a movement that was already applied, the operation becomes idempotent: applying it once or ten times gives the same result.
The client connection
A Nuxt composable opens the connection and forwards each message to the store:
export function useStockSocket(url: string) {
const stock = useStockStore();
let socket: WebSocket | null = null;
function connect() {
socket = new WebSocket(url);
socket.addEventListener("message", (event) => {
stock.applyMovement(JSON.parse(event.data));
});
socket.addEventListener("close", () => {
setTimeout(connect, 2000);
});
}
onMounted(connect);
onBeforeUnmount(() => socket?.close());
}The forgotten case: reconnection
A WebSocket connection always drops eventually: unstable network, laptop going to sleep, server restart. During the outage, movements may have been recorded without the client receiving them.
Reconnecting is therefore not enough. After every reconnection, reload the full stock state from the API, then resume listening for events. Otherwise the screen looks live while showing wrong numbers, which is worse than a screen that is visibly disconnected.
Key takeaways
- The database remains the source of truth; the WebSocket only notifies.
- Broadcast after the write, never before.
- Make applying movements idempotent with a unique ID.
- Reload the full state after every reconnection.
These four rules apply well beyond inventory: for a dashboard, a chat or live auctions, the problem is the same.
