Making It Feel Like Real Software (and Locking It Down)
Redesigning every screen for the shop-floor monitor, adding sale voids, and the firewall gotcha that was quietly exposing my API to the whole internet.
By this point the system worked. It also looked like a pile of forms an engineer threw together at 1am, because that’s exactly what it was. This stretch was about two things: making it feel like software people trust, and making sure the thing I’d trusted to a private network was actually private.
Designing for the actual screen
The branch apps don’t run on my laptop. They run maximized on a shop-floor monitor, usually a modest 720p one, all day.
So I stopped designing for “some window” and redesigned every screen for exactly that: a fixed 720p layout, maximized, non-resizable, laid out so the coordinates I place in the designer are literally the pixels on the shop’s screen. No responsive guessing. The buttons are where the buttons are.
I also finally deleted the old legacy projects — the pre-cloud CSV uploader and its dead local database. They’d survived only out of sentiment. Cutting them felt like clearing a workbench.
Void, not “correct”
The owner had one very clear request about fixing mistakes at the counter: whole-sale void only. If a sale is wrong, kill the whole thing and ring it again. No editing individual lines, no partial corrections, and — emphatically — no receipt printing.
That last one surprised me until I sat with it. Line-level “corrections” are exactly how skimming happens and how honest mistakes get muddy. A clean void is auditable: the sale existed, then it was voided, both facts on the record. Simpler is safer here, and it’s the owner’s money, so simpler wins. I added the void and a branch-side sales history to go with it, and left receipt printing firmly out.
I also built a second POS this week — one for the office counter itself, since the office is both the delivery hub and a walk-in shop. It reuses the exact same offline queue-and-sync machinery as the branch POS, just pointed at the office’s own stock and showing a different slice of products. Copying a proven pattern beat inventing a second one.
Then I checked the front door
Here’s the part that made my stomach drop.
My entire security model rested on one belief: the API is only reachable over our private VPN. The server’s firewall was configured to allow traffic on the VPN interface and nothing else. I’d checked. It was correct.
The API was still reachable from the open internet.
The reason is a genuinely nasty interaction I didn’t know about. The container runtime writes its own firewall rules, and it inserts them ahead of the operating system’s firewall. So when I published the API’s port with the naive “just expose the port” setting, the container quietly bound it to every network interface and punched its own hole — one that sat in front of the OS firewall rule that was supposed to be protecting me. The firewall said “VPN only.” The container said “actually, everyone,” and the container went first.
The fix was one small change: bind the published port to the private VPN address specifically, not to everything. Now the port simply doesn’t exist on the public interface. But I want to be honest that this was live for a while, and I only found it because I went looking on purpose. “The firewall allows only the VPN” was true and meaningless at the same time.
Defense in depth
That scare pushed me to stop treating “you’re on the VPN” as the whole security story. Being on our network is necessary, but for the sensitive actions it shouldn’t be sufficient.
So I layered on device-level allowlists. The office-only actions (buying stock, creating deliveries, editing the catalogue) only accept requests from the specific office machines. The owner-only actions (changing recipes, setting selling prices) are stricter still — only the owner’s own two devices. And branch actions are gated to that branch’s known machines.
The nice property is that these stack on top of the VPN, they don’t replace it. And I deliberately let branches that aren’t onboarded yet stay ungated — locking out a branch that’s mid-operation to feel more secure would just break real sales, which, per the last post, is the one thing I never do.
Somewhere in here I also chased down a couple of genuine race conditions — two people acting on the same delivery at the same instant, one deleting while the other accepts. The cure was the unglamorous, correct one: take a lock before you touch shared stock, and serialize. Not clever. Just right.
The system looked like software now, and I finally believed the network story instead of just hoping it. Next: getting all of this out of Windows-only desktop apps and into a browser.