This week I deleted a cloud job runner and moved the work onto a Mac Mini sitting on a shelf in my house. That sentence sounds like a downgrade. It was the opposite, and I want to explain why, because the reasoning is more interesting than the wiring.
Some background. I run three things that used to be three separate codebases: a site builder (people spin up a website, it comes with themes and SEO and legal pages), a personal automation agent that lives on the Mac Mini, and a writing app. Three repos, three deploy stories, three different ideas about where "the AI part" should run. The site builder did all of its AI work in the cloud through a background job service, calling GPT-4o for everything: theme generation, layout suggestions, SEO analysis, legal document drafting. Eight background jobs in total, five of them AI, three of them plumbing (sweep stale jobs, verify domains, pull search-console metrics).
The first move was boring on purpose. I pulled all three projects into one monorepo, with full git history preserved, one lockfile, one build graph. The rule I gave myself was "one variable at a time." The site builder is on an older major version of the framework than the other two apps, and everyone's instinct in a consolidation is to bring everything to parity in one heroic sweep. I did not. Merging three repos is already enough moving parts. Upgrading a framework in the same week would mean that when something broke, I would not know which change broke it. So the version gap stays, deliberately, until the merge is boring and I can change exactly one thing and watch what happens.
Then the real move: taking the AI off the cloud.
Here is the thing that makes it worth writing about. The Mac Mini already runs an agent I trust with real work. It has a Claude Pro subscription attached to it, a worker loop that polls a queue and spawns Claude Code with full tool access, and a scheduler that already does periodic maintenance. It is, functionally, a job runner I own outright. The cloud service I was paying for was doing a strictly smaller version of what the Mini already does, except metered, except through an API key, except somewhere I cannot SSH into at 1am when something is wedged. Once I saw it that way, keeping the AI in the cloud stopped looking like the safe default and started looking like rent.
So I built a seam. The site builder no longer contains any AI code. It enqueues a job that says "analyze the SEO for this site" and hands it off. On the Mini, a handler catches that job by name, gathers the same context the old cloud task used to gather (the site, its pages, its sections, its top search queries), builds a prompt, and lets Claude Code do the analysis. The result gets written back into the exact same table shape the old system wrote, so the site builder's UI never knew anything changed. I did all five AI jobs this way, one at a time, each behind a feature flag with the old cloud path sitting right next to it as an instant fallback. If a migration misbehaves, I flip one flag and I am back on the cloud in seconds. None of it was a leap of faith. All of it was reversible.
Two details from this week that I think are the actual craft, the parts that do not show up in a commit message.
The first is a constraint I refuse to bend: spinning up a site can never wait on AI. Ever. A site is created instantly with sensible defaults, and the AI results (the theme, the SEO pass, the legal pages) arrive later and upgrade the site when they are ready. If the Mac Mini is offline, or the queue is backed up, or Claude is having a slow afternoon, site creation still works, exactly as fast as before. The AI is an upgrade that lands asynchronously, not a gate you stand behind. That single rule shaped the whole architecture. It is why the handoff is a queue and not a function call.
The second is that the Mini's worker is single-threaded, and that changed how I ported one of the plumbing jobs. The old cloud version of domain verification would start, then sit in a 30-second polling loop hammering an API until the domain verified or timed out. On a cloud service with unlimited parallel workers, a job that naps for 30 seconds costs nothing. On my single-threaded worker, a job that naps for 30 seconds blocks everything else in the queue behind it. So I turned the loop inside out. Instead of one job that waits, every scheduler pass advances all pending domain checks by exactly one API call and moves on. Same outcome, but nothing ever blocks. DNS propagation takes minutes anyway, so checking every couple of minutes instead of polling in a tight loop costs nothing real and keeps the worker free. That is the kind of thing you only notice when you actually own the runtime instead of renting it.
I am not going to pretend this is the right call for everyone. If I had a team, or spiky traffic, or a pager rotation, "run it on a Mac Mini in my house" would be a genuinely bad answer. But I am one person, my traffic is not spiky, and the reliability story I care about (site creation never breaks) is guaranteed by the architecture, not by the hardware. What I got in exchange is control, one bill instead of two, and a system where every moving part is something I can open, read, and fix.
The cloud was a good default. It just was not mine. This week it became mine.

