← all writing

I built the MDM instead of buying it

So I got handed a box of industrial Android scanners. If you don't know the type, it's basically an Android phone with a barcode gun bolted to the bottom. You've probably watched somebody wave one at your stuff in a store. I have to admit, before this I'd never touched a mobile computer in my life, and I'm not an Android guy. Last time I used Android it was on version 3.

Here's where it started going sideways. I poke around the devices and they're still on Android 10. That feels like a problem, so I go looking for a way to update them. Turns out these are commercial devices, and everything is tied to an account with the manufacturer, or with whoever you bought them from. We bought ours from a supply house that went out of business, for pennies on the dollar. So who even knows if those accounts still exist. Right there I made the first real call of the project. We go with what we have and make it work.

That sent me down the rabbit hole of the manufacturer's own software for managing these things. I'm sad to report it was a bust. The management server only runs on Windows, and it's clunky. More Windows software means more Windows updates. More servers to babysit. More security holes. More software I'd set up once and then forget how it works six months later when I need to add another device. Sitting there looking at it, I had the thought that gets a lot of engineers in trouble. These are just Android phones. Let me see what I can do.

That thought is the whole post, really. The honest version of "build vs buy" isn't "always build." It's that the thing I was being sold cost me more than the thing I'd build, once you count the Windows servers and the updates and the six-months-later confusion as part of the price. The SaaS wasn't free. It was just charging me in a currency that doesn't show up on the invoice.

#First, back to a clean slate

Before any of that, I had to get the devices back to a clean slate, because the company that owned them before us left them exactly as they were. It turns out the factory reset on these isn't a menu item. You crack the device open, and under the keypad there's a microSD slot, and that's the path back to vanilla. Did that. Then I rinsed and repeated it for the rest of the box.

Now I've got a stack of plain Android 10 devices and a blank page.

#Design for the person who didn't ask for this

The first thing I had to get straight was who's actually holding these. Some of the warehouse crew are sharp with a phone. Some of them are closer to the flip-phone crowd, and I mean that with love. The whole point was that none of that should matter. Whoever picks up a device should get exactly the tools the job needs and nothing else in the way. I'm an iPhone guy myself, and I didn't want some poor picker getting tripped up by Android any more than I wanted to get tripped up by it.

So I built a kiosk. On Android, if your app is the Device Owner, you can replace the home launcher entirely, so I did. The stock launcher is gone. What a worker sees is a plain landing screen with the two or three things they actually touch, a shortcut into our ERP's warehouse module, a shortcut to our intranet, and a way to log in. The kiosk lock pins them to that. No settings to wander into, no app store, no 99% of Android that nobody in a warehouse needs. Lock-task mode, a pile of pre-installed apps suspended, the consumer Google apps hidden, and my app set as the persistent default home so a reboot lands them right back where they belong.

#Then it grew a brain

A kiosk on a device is fine until you have a dozen of them spread across a building, and one of them is acting up, and you're not standing next to it. So the next piece was a place for them to phone home to. I wrote a small Go server. The devices check in over mutual TLS, each one carrying its own client certificate that the server minted, so a device can't talk to the fleet unless it's really one of ours. They heartbeat, they poll for commands, they ship their logs back. It quietly turned into a light MDM, with all the logging and error-checking you'd hope for and usually don't get from the thing you bought.

I started catching battery health too. Each device reports its battery serial and its state, so I can see which packs are circling the drain before a worker is stranded on the far end of the warehouse with a dead battery.

Then the question of who has which device. We already use a four-digit code internally to sign off on work as it moves through the building, basically a "who did what" trail. So I just tapped into that. A worker types their four-digit code, the device shows their name on the screen, and on my side that's the moment I log who's holding which device. The codes hash on the way in, and there's an offline cache so the lockscreen still works if the WiFi hiccups, because a warehouse is exactly where the WiFi hiccups.

#The IT tools, hidden in plain sight

Once the bones were solid I went a little wild on the diagnostics, because I knew I wasn't always going to be in the building. There's a full device-info readout, a keyboard-and-scanner test, a touchscreen test, a WiFi signal test, and a maintenance mode that keeps somebody from wiping a device by accident. All of it tucked behind a secret gesture I'm obviously not going to print here, on the off chance one of my pickers reads this and goes treasure hunting. Let's just say it's up, up, down, down, left, right, left, right, B, A, and leave it at that. Invisible to a worker, one secret handshake away for me.

The one I'm proudest of is remote screen view. A worker calls with "it's doing the thing again," and instead of walking across the building, or worse, driving a couple hours to another site, I can just pull up what's on their screen. Android makes you fight for that. Screen capture throws a system permission dialog, and a kiosk device has nobody around to tap "Start now." So there's a small accessibility service whose whole job is to notice that dialog and confirm it, so the capture starts on its own. It's the kind of duct-tape that feels dirty until it saves you a four-hour round trip.

#The part I'd put on a billboard

One of our managers mentioned, kind of offhand, that it'd be nice to push a message to the devices. So I built that, a full-screen note the whole fleet sees with a button that acks back so I know who read it.

But the piece I keep bringing up is the MCP server. I gave an AI agent the keys to the fleet. The same server that runs the dashboard also speaks MCP, so I can sit in Claude and say "what's the battery on the device in shipping," or "ring device three, somebody set it down," or "show me the last hour of errors," and it just does it. Plain language in, real fleet actions out. I added a regular API alongside it so our intranet can reach the same tools, the messaging especially. Once your operations surface is something an agent can drive, the gap between "I noticed a problem" and "the fix is happening" basically closes.

#The stuff that fought back

I won't pretend it was clean. The browser was a saga on its own. The build-in app engine I started with kept choking on the warehouse app's popups, so I pivoted to launching the real browsers the device already had. Except the stock browser on a device this old is ancient, years out of date, so I had to sideload a current one and the shared engine library it depends on, with the versions matched exactly or it just refuses to start. Then there's the scanner itself. Getting the scan trigger to type the right terminator, a Tab here, an Enter there, meant wrestling the vendor's enterprise config SDK, which has its own pile of undocumented gotchas that I now have written down so the next person doesn't lose the afternoon I lost.

And the devices update themselves over the air now. The agent pulls a new build over mutual TLS, verifies it, and installs it as the Device Owner, which lets it get past the "no installing apps" lockdown that's pointed at everyone else. One version bump on the server and the whole fleet rolls forward on the next check-in. I gate it behind a version flag so nothing moves until I say go.

#Then it went live

Then the day came. We had a rep from our ERP vendor on site to walk us through cutting over to wireless. Anybody who's done one of these knows you can only prep for so many of the unknown unknowns, and we'd covered most of the bases. I spent one day in a conference room making edits to devices, then a day on the warehouse floor making changes and answering questions and modding my own code on the fly, then a third day on cleanup. We did it. The transition is done and the system works.

There was a lot of stress in there. I also learned, the hard way, that trying to write code in a loud busy warehouse is not where I do my best work. But I walked out feeling like I'd pulled off something that shouldn't have been possible. I took a box of old hardware nobody wanted and turned it into a system I'd be comfortable handing to somebody else.

#When build beats buy

I could've gone to my team and said we need a SaaS to manage all this. It wouldn't have been true, and we'd have paid for it twice, once on the invoice and once in control we gave away. Instead we've got a tiny Go app that'll run on just about anything, that does exactly what we need and not one thing more, and yes, it has a dark mode.

That's the whole 0-to-1 of it. I needed a thing, so I made the thing. And I built it on purpose to be handed off, documented and boring and durable, the kind of system that keeps running for years without somebody standing over it keeping the gears greased. If a tool only works while you're holding it, you didn't finish it. This one's finished.


← all writing