React Native on kiosk hardware, serving 7M+ passengers a year.

Kiosks are not phones. There is no user to retry or reboot when something fails, a payment can be mid-flight when the network drops, and hardware fails in ways software can't ignore: printers run out of paper, cash units jam, terminals time out. The app has to run for weeks without a restart, in a public space, under the time pressure of a train departure. Every edge case that a mobile app can push back on the user, a vending machine has to resolve on its own.
The machine drives four classes of hardware from a single React Native app: VayaPay payment terminals, CashGuard cash-handling units, receipt printers, and QR scanners. Each lives behind its own abstraction layer, so the UI never talks to a vendor SDK directly. That kept the interface testable without physical hardware on the desk, and it meant a vendor could be swapped without touching a single screen.


Not every ticket starts at the machine. Passengers with a pre-purchased booking can scan a QR code at the TVM to fetch and print their ticket. Behind the scan, a WebSocket connection keeps the machine and the backend in sync while the ticket is located, validated, and issued, so the passenger watches live status instead of a spinner and stale state never reaches the printer. Building real-time flows on a machine that must also survive connection loss forced a clear separation between what the socket reports and what the machine commits to.
The scenario that shaped the architecture: payment captured, ticket not issued. The system detects the mismatch and refunds automatically through VayaPay, with no staff involvement, so the worst case for a passenger is a refund notification instead of money lost to a machine. Printers were the second battlefield. Paper runs out and print heads fail, and they usually do it right after a successful payment, so every print is verified and every failure path lands somewhere safe: the ticket is recoverable, or the payment is returned.
Node.js and MongoDB services handle payments, refunds, and ticket data. A queue manager built on Azure Functions carries the work that must not block a passenger: failed integration calls are re-queued and retried with backoff instead of failing the purchase, and payment reconciliation — settlement, refunds, reporting — runs asynchronously in the background. The machine stays responsive at the front while the queue absorbs the mess at the back.
An unattended kiosk can't tell you it's sick, so Datadog monitors the fleet and alerts on failures before passengers queue up behind them. Amplitude tracks the purchase funnel to show where people hesitate or abandon. And because Flytoget serves every kind of traveler, the interface is built for accessibility: a dedicated accessibility mode, clear contrast, and language support, designed for someone using it for the first time with a train to catch.

Offline handling should have been a first-class design concern from day one. It arrived later than it should have, and retrofitting degraded-mode behavior into flows built with a reliable network in mind is far harder than designing for it up front. It's the first thing I'd whiteboard on any future unattended system: assume the connection is gone, then decide what the machine is still allowed to promise.