Qnack + qnack-mqtt-print: Bridging a Web POS to Thermal Receipt Printers
Qnack is a web-based point-of-sale for hospitality; qnack-mqtt-print is the small Node service that bridges the browser to thermal receipt printers over MQTT. The architecture, the MQTT topic design, and the failure modes we had to handle.
A browser-based POS that talks to a thermal receipt printer over USB is a non-starter: USB access from a browser tab is restricted, hostile environments do not install drivers, and the printer might be in the kitchen while the order is taken at the bar. We needed a transport that the browser could reach and that a tiny headless service could speak natively. MQTT was the answer.
The architecture
Three components, each minimal:
- Qnack web POS. A Next.js app that runs on a tablet at the bar. Order entries fire an HTTPS request to the Qnack backend with the order payload.
- Qnack backend (NestJS). Persists the order, broadcasts a "print" event to an MQTT topic scoped to the venue.
- qnack-mqtt-print. A small Node service running on a Raspberry Pi next to the printer. Subscribes to the venue-scoped topic, formats the order into the printer's ESC/POS byte stream, and writes it to the printer over USB.
Why MQTT, not WebSocket
WebSocket would have worked, but the kitchen and the bar are on the same physical network as the printer — and not on the same network as the cloud backend. MQTT brokers can run locally on the Pi, behind a firewall, with no inbound ports from the internet. The order survives a backend deploy, a brief internet outage, or a tablet reboot. The publish is local-first, durable across reconnects, and acknowledged at the broker.
Topic design that scales
The topic design is the part most teams underinvest in. Ours:
qnack/{venueId}/printers/{printerId}/jobs— the queue for a specific printer in a specific venue.qnack/{venueId}/printers/{printerId}/status— last-will-and-testament heartbeat, so the POS can show "kitchen printer offline" if the Pi stops heartbeating.
A new venue is a new branch on the topic tree. The broker enforces access control per venue, so a misconfigured subscriber at venue A cannot read venue B's order stream.
Failure modes we had to handle
- Pi power cycle. The MQTT client reconnects on boot, the last-will-and-testament publishes a "printer offline" message, and the POS stops queuing orders against this printer.
- Printer out of paper. The ESC/POS driver returns a status byte. qnack-mqtt-print publishes a "printer error" message on the status topic, and the POS surfaces a banner.
- Backend unreachable but POS still online. The Qnack POS queues orders locally and retries with backoff. The kitchen printer still gets them once the backend recovers.
What this taught us
The lesson generalizes beyond POS: when you need browser-to-physical-device communication, the bridge service is the boring part that everyone underestimates. Build it as if it will outlive every frontend rewrite — because it will.
