The feature that sells CR Cafe in a demo is simple to describe: click Lock on the dashboard and the PC across the room locks before you look up. Making that feel instant, and making it safe, took more design than any other part of the system. This is how it works.
Why polling was never an option
The obvious first version has each PC ask the server every few seconds whether it should lock. That makes every command wait for the next poll, multiplies request volume by the number of stations, and still can’t tell the dashboard the moment a PC goes offline. A persistent connection solves all three: the server pushes a command the instant it’s issued, and a dropped socket is the offline signal.
One hub, two kinds of client
The Node.js backend runs a Socket.IO hub that relays between two client types, told apart by a type field in the handshake. Station agents (the Electron app on each PC) connect with their station and cafe IDs; admin dashboards connect with their cafe ID and an authenticated session cookie. The hub keeps an in-memory map of live agents keyed by station, and pushes status changes to every dashboard in the same cafe.
dashboard ── admin:command {stationId, "LOCK"} ──▶ hub
hub ── command {"LOCK"} ──▶ agent on that PC
agent ── agent:lock-changed {isLocked: true} ──▶ hub
hub ── agent:lock-changed ──▶ every dashboard in the cafeProving who is on the other end
A station agent could claim to be any station. If the hub believed it, a rogue client could evict the real agent from the map and start receiving commands and credentials meant for someone else. So the handshake checks two things: that the station ID actually belongs to the cafe ID, and that the agent holds the right secret for that station. Each PC is issued its own secret on first connect, falling back to a cafe-wide one until it has proved it holds its own — which let live cafes migrate without re-enrolling a single machine.
The bug: a socket is an API too
Our REST routes had always checked permissions. The socket handlers originally checked only that the dashboard belonged to the cafe. That meant an employee without station permissions could do over the socket what the REST API would refuse. The fix was to give every admin:* handler the same permission check as its REST twin, and to answer a refusal with an explicit admin:denied event the dashboard can show.
function allow(socket, action, permissions) {
if (socketCan(socket, permissions)) return true
socket.emit('admin:denied', {
action,
message: "You don't have permission to do that.",
})
return false
}Acknowledgements make the reply mean something
Early on, some commands reported success as soon as the server had sent them. A wrong MAC address or an agent on the wrong network still read as “done”. Commands that can fail on the station now carry a request ID and wait for the agent to answer with a result, which is what the dashboard shows. We wrote about the hardest case — waking PCs that are switched off — in Wake-on-LAN for a whole cafe.
Only a key crosses the wire
Launching a game from the dashboard sends a short key like valorant, never a path to an executable. The agent looks the key up in its own local allowlist. A compromised dashboard or socket therefore can’t make a PC run an arbitrary program — it can only ask for things the PC already agreed to run.
What we would tell another team
- Treat every real-time event as an API endpoint with its own authorisation
- Authenticate devices individually, not with one shared secret
- Make results come from the device, not from “the message was sent”
- Send identifiers over the wire and resolve them locally
The full architecture is in the CR Cafe case study. If you need real-time features in your own product, see our web application development service.
