Running a 31-screen retail network across four stores
A POS, a 31-device Fire TV signage fleet, and a menu designer for a four-store cannabis retailer, all wired into one system we build and run remotely over ADB and VPN.
Picture the manager of a cannabis store on a step ladder, phone wedged under one ear, trying to plug a USB stick into the back of a TV that is bolted to the wall above the register. The menu on that screen has been wrong for three days. The price changed, the product sold out, and the only way to fix it is to physically touch the screen. Now do that math across four stores and thirty-one screens. That is the problem Craft Cannabis came to us with.
Craft runs four retail stores. Like most retailers, their screens, their registers, and their menus all lived in separate tools that did not know about each other. Updating signage meant visiting each TV by hand. The menu boards drifted out of sync with what was actually on the shelf. Building case cards and printed menus was manual work that ate hours every week. We replaced that pile with one system: the POS, the inventory, and every screen wired into one network we build and run remotely. Here is how it actually went together.
Four surfaces, one source of truth
The whole thing hangs on a single idea: the register, the kiosk, the menu boards, and the case cards should all read from the same place. For Craft, that place is POSaBIT, their compliance and inventory platform. If everything pulls from one source, the screens cannot disagree with the shelf.
So we built four surfaces against that one source. A Windows POS and kiosk app on Electron 40, React 19, and better-sqlite3, wired into the POSaBIT API v3. A 31-device Amazon Fire TV signage fleet running an Android app in Kotlin. A web menu and case-card designer we call CaseCard. And the deployment tooling that ties the fleet together. Four front ends, one inventory truth behind all of them.
The better-sqlite3 piece matters more than it looks. Retail networking is flaky, stores lose their connection, and a register that freezes when the internet hiccups is worse than no register at all. So the POS keeps a local cache and keeps ringing sales whether or not the link to POSaBIT is up. The kiosk app is the same binary running in kiosk mode, so what staff see and what customers browse come from the exact same code.
Thirty-one screens that update themselves
The Fire TV fleet is the part people notice. Thirty-one devices spread across four stores, and they are not all doing the same job. Some hang over the check-in desk and run in queue mode, a WebView pointed at the check-in queue. Most hang over the floor in screen mode and run the offline menu display. The mode is set per device, so the same APK behaves differently depending on where it is bolted.
The rule we held to: a screen that goes up should stay current on its own. No staff action, no store visit. Every device checks for an update and installs it silently, every day. A price change in POSaBIT flows to CaseCard, gets approved, and lands on the boards without anyone climbing a ladder.
The current spread looks like this:
- Tacoma: 9 devices (3 queue-mode check-in screens, 6 menu screens)
- Mill Plain: 11 devices, all menu screens
- Andresen: 6 devices, all menu screens
- North Wenatchee: 4 devices, all menu screens
Thirty devices live, with a couple of stragglers still pending an IP or an ADB handshake. Which brings us to the unglamorous part.
Deploying a fleet over ADB and VPN
Fire TV is Android, so the honest way to manage thirty-one of them is the same tool Android developers use on a single phone: ADB, the Android Debug Bridge. We built deployment tooling around it. A devices.json registry that knows every screen's IP, store, and mode. A set of ADB scripts that push the APK and flip settings. And a manager dashboard, a single HTML page, that runs the whole fleet from one screen.
The connection back to the stores runs over a VPN into each store network. From one laptop we can reach every device across all four locations, push a new build, and confirm it landed, without driving anywhere. When v1.5.0 of the menu app ships, it goes to all thirty screens from the same place.
It is not magic, and the rough edges are real. ADB's security model means a brand-new device pops an "Allow USB debugging from this computer?" prompt the first time you connect, and that prompt lives on the TV, not your laptop. So the very first deploy to a new screen needs someone physically on site to tap Allow on the remote. After that handshake, the device is ours to manage forever. That single manual step is why the deployment notes track devices as "pending IP" or "ADB refused" until someone is standing in front of them.
There were Windows papercuts too. At one point a stray file named nul showed up in a build directory, a Windows reserved name that normal delete commands will not touch. The fix is the kind of thing you only learn by hitting it: robocopy an empty directory over it. None of this is in a tutorial. It is what operating a real fleet actually costs.
CaseCard: menus that match the shelf
Signage that updates itself is worthless if the content is wrong, so the menus themselves needed their own system. CaseCard is a web app for building case cards and printed menus, with reusable templates that keep every store on-brand and an approval step before anything goes live. Nothing reaches a screen or a printer until someone signs off.
CaseCard pulls from POSaBIT through a dual API: the v1 endpoints for full inventory detail and v3 for tags and lazy loading, because pulling a four-store catalog in one shot is not something you want to do on every page load. The result is that the menus reflect real inventory instead of a stale spreadsheet, and because the POS reads the same POSaBIT data, the register, the kiosk, and the boards finally agree on what exists and what it costs.
Updating signage used to mean touching each TV by hand. Now a price change in inventory flows to the boards on its own, across four stores, and the only person who has to think about it is the one who approves the menu.
What it took, and what we learned
The stack ended up spanning more surfaces than a single product usually does: Electron 40, React 19, TypeScript, and better-sqlite3 on the desktop side, Android and Kotlin on the screens, the POSaBIT dual API in the middle, and ADB-driven fleet tooling over the top. The hard part was never any one of those. It was making them behave like one system instead of four projects.
A few things we would tell anyone building retail infrastructure like this:
- Pick one source of truth early and make everything else read from it. Every "the screen is wrong" bug traces back to a second source of data nobody meant to create.
- Design for offline from day one. Stores lose connectivity, and a register that stops ringing sales is a real-world emergency, not a stack trace.
- Remote fleet management is mostly boring plumbing (a device registry, scripts, a dashboard) plus a handful of physical gotchas you can only plan around, not automate away.
- Silent daily auto-updates are what separate a fleet you operate from a fleet you babysit.
We built it, then we run it
Thirty-one screens are live across four stores, run as one system and operated remotely. The POS, the inventory, the menus, and the signage all pull from the same source, so what staff ring up, what customers browse, and what the boards show finally line up. Updates ship to every device from one dashboard, and the stores never have to think about it.
This is not a project we delivered and walked away from. It is a system we built with AI and still run in production, and it keeps getting better instead of rotting after launch. That is what we mean by your business, on autopilot.
Wondering what one system would replace for you?
