Home Infrastructure: A Family Operating System
Three Workspaces, Twenty Services, One Family

The Setup
I had a Proxmox homelab running about twenty self-hosted services: Immich for family photos, Nextcloud, Vaultwarden, Open WebUI, Tandoor recipes, Baby Buddy, Ghostfolio for portfolio tracking, and a dozen more. The front door was Dashy, a link dashboard that showed a grid of bookmarks and a few basic widgets. It worked, technically.
The problem with Dashy is that it's too spread out and hard to build secure integrations around. Everything is frontend code, which means secrets are effectively public. It's hard to extend with complex features, and honestly kind of slow. But the real issue was simpler: everyone forgot it was there. People just went directly to the one or two services they used, and the rest was forgotten. Our extended family contains citizens of four countries. We speak multiple languages at home. And the digital side of our household was a collection of disconnected services behind a patchwork auth stack that I'd grown organically. I'm still the only one who understands this stuff, but it's much less complicated now and more secure than hiding everything behind SSO.
I didn't want a better dashboard. I wanted a family operating system. Here's what that means:
The network: Every device on a private Tailnet mesh VPN via Headscale. Internal traffic never leaves the mesh. Everything is VPN-only — the only public-facing endpoint is a tiny onboarding invite link. Behind the VPN, a custom Caddy build handles routing with caddy-security for OIDC and replace-response for injecting the StataBar sidebar into every authenticated page. One login for everything, powered by Zitadel.
The services: Nextcloud for file sync, Immich for photos, Vaultwarden for passwords, Tandoor for recipes, BookLore for ebooks, Open WebUI for a private family AI chat (MCP server connecting all data — every query stays in the family), Baby Buddy for infant tracking, Ghostfolio for portfolio management, Forgejo for git and CI/CD — fifteen services total, each on its own subdomain, each with SSO.
The data feeds: Nine plugins feeding real data from official sources. DWD weather with 240-hour forecast and live radar. BVG transit departures with geolocation. BSR garbage collection schedule. ECB exchange rates. German public holidays merged with family birthdays. Ghostfolio portfolio with alpha/beta against benchmark. Baby Buddy with circadian-aware feeding predictions. World clock with timezone overlap planner. DHL postal tracking.
The apps: A native Android app (StataApp) with Glance homescreen widgets — transit, weather, baby, portfolio, garbage, holidays, timezone. A TypeScript web component (StataBar) injected via Caddy into every authenticated service as a navigation sidebar. A Rust API backend (StataAPI) hosting all nine plugins with a shared cache layer. And a four-language onboarding wizard (EN/DE/PL/HU) that walks new family members through VPN setup, app install, account creation, and service discovery — complete with annotated video recordings.
Zero cloud dependencies. All data sourced from official authorities. Family photos behind encrypted VPN. No Google, no Meta, no third-party intermediaries.
Activity During the Build
- High ceremony per action
- 453 auth msgs / 427 Caddy msgs despite low error rates
- Infrastructure ops only
- Blue/green deploys via Forgejo CI
- 316 consecutive AI messages (config)
- 267 consecutive AI messages (holidays)
- Architect's desk: specs drive implementation
- Sub-agent dispatch hub
- Playwright + ADB for visual verification
Phase 1: Vision Day | Mar 3-4 | Sessions 1-2 | The entire feature set specified before a line of implementation code
Interview-Driven Design
I started the way I usually do when I have a vague idea and too many possibilities: I told Claude to interview me.
What followed was the full system design: a Rust API backend (StataAPI) with a host-driven plugin architecture, a TypeScript web component (StataBar) injected into every authenticated page via Caddy's HTML rewriting, an Android app (StataApp) with Kotlin/Compose and Glance homescreen widgets, and an onboarding wizard for non-technical family members.
I pasted my entire Dashy config: every service, every widget, every exchange rate pair. Then I started making decisions: plugins are passive data adapters, not autonomous actors. The host owns caching, HTTP, error handling. Plugins declare routes and transform data. I wanted this specifically because it makes testing straightforward: mock the host, not HTTP.
The plugin interface is small by design:
/// A data plugin declares its routes and how to parse upstream responses.
/// Plugins are passive — they never make HTTP calls directly.
pub trait DataPlugin<F: Fetcher>: Send + Sync {
fn name(&self) -> &str;
fn prefix(&self) -> &str;
fn routes(&self, state: AppState<F>) -> Router;
}
And the Fetcher trait that makes it all testable — object-safe via boxed futures so we can use Arc<dyn Fetcher> in AppState:
pub trait Fetcher: Send + Sync {
fn get(&self, url: &str)
-> Pin<Box<dyn Future<Output = Result<FetchResponse>> + Send + '_>>;
}
The monorepo structure reflected these boundaries:
stataapi/ — Rust/Axum backend (plugin host + 17 plugins)
statabar/ — TypeScript web component (Shadow DOM, zero npm deps)
stataapp/ — Kotlin/Compose + Glance widgets
gvl-onboard/ — Rust/Axum onboarding wizard
specs/ — Design specs that drive everything above
features/ — Per-widget feature specs
agentic.md — How Claude agent teams should operate
The Agentic Spec
Before any implementation, I wrote specs/agentic.md, a document defining how Claude agent teams should operate. Model selection rules: Sonnet for routine tasks and log parsing, Opus for architecture. Coordination protocols. What not to do. When to stop and ask.
I have management experience and it probably helps me think about how to break up work and context-window overflow — which is oddly similar to managing teams with limited bandwidth. But the techniques get different parameters with agents. I give agents less training and more supervision, more guard rails about what to do. I can't evolve an agent to one day become a manager, but I can create a manager that handles some slice of complexity for me. It's different, but some skills overlap.
The Massive Build Session
Session 2 was 324 messages. I launched agentic teams in waves: framework agents first (one per system: API, sidebar, app), then feature agents cross-system (weather, transit, baby, portfolio, holidays, garbage, timezone), with an architect agent reviewing plans before approval. The monorepo scaffolded in a day: Rust API with 17 passing tests, TypeScript sidebar with Shadow DOM isolation, Android app with 7 Glance widgets, and a marketing page with Playwright-generated screenshots.
Seeing all the screenshots and the flow just working was genuinely exciting. The defaults seemed to land, which was really cool — the system looked like a real product without me fiddling with every pixel.
Phase 2: Infrastructure Sprint | Mar 11-13 | Sessions 3-10 | The air-gapped relay, Zitadel OIDC, domain migration
The Relay Tax
The homelab workspace is where this project diverges from every other case study I've written. I specifically do not want Claude on my Proxmox host. There's no tmux session, no direct file access. I am the relay.
Claude generates pct exec commands. I review them, run them on the host terminal, and paste the output back. Every action has ceremony: read the command, evaluate whether it's safe, execute, copy output, paste, wait for analysis, repeat. This is why Auth (453 messages) and Caddy (427 messages) were the two biggest message-count topics despite having among the lowest error rates (0.1 errors per message). The messages weren't fixing problems. They were the cost of operating through a human relay.
I considered giving Claude direct access, but Claude made some mistakes I would not want to go chase after cleaning up in my production hypervisor. I think infrastructure-as-code with an ephemeral environment would have let me unlock Claude to just run in a loop and deploy cleanly, saving hours. But I don't have it, and setting that up costs time itself. Any business that regularly changes infrastructure should invest in that. My needs aren't big enough to justify it — so I stayed the relay.
Zitadel: Doing It Right
The old auth stack was worse than a layer cake: Internet → Caddy (host) → Nginx (host) → BunkerWeb (its own container) + Authelia (its own container) → destination service. For the new system I chose Zitadel, a proper OIDC identity provider, and Caddy with the caddy-security plugin, collapsing all those layers.
The .home TLD caused real problems. It's a reserved TLD, but not all software handles it correctly, especially on tailnet where DNS resolution has its own opinions. Claude kept suggesting header workarounds and database modifications.
In the end I bought a .com domain and ran everything from that. I automated OIDC app creation for all apps with a shell script (create-oidc-app.sh) rather than clicking through the UI for each of the 8+ services: Immich, Ghostfolio, Vaultwarden, Baby Buddy, Open WebUI, Tandoor, BookLore, and more.
The Domain Migration
Mid-project, I decided to migrate everything from *.gvl.home (internal step-CA, only works on the tailnet) to the family domain with real Let's Encrypt certificates. The internal CA had been causing trust headaches on Android and new devices, and I was maintaining split identity across two domains.
I wrote six migration scripts (phase0-prep.sh through phase5-cleanup.sh) and executed them while simultaneously debugging breakage: BunkerWeb 403s, Headscale crashes, SSL cert spam from Caddy. Every service's config had to change. It was a single intense day on the homelab, all through the relay pattern.
I had to review everything. There were elements of blind trust if I knew the error was easily reversible. Having 3-2-1 backups helps — you can be bolder when you know you can roll back.
Phase 3: The Parallel Grind | Mar 16-21 | Sessions 11-25 | Peak multiplexing, feature development, trust profiles in action
69 Switches in a Day
March 20 was the peak: 69 context switches between home-app and private-ecosystem in a single day. This wasn't project-to-project multiplexing like my other work. This was switching between workspaces within the same system. Different projects going on with different context setups per host. Slow-burn stuff on tmux, rapid stuff that needed horsepower on the big desktop.
Trust Profiles in Action
This is where the three workspaces revealed their true characters:
The tmux server (private-ecosystem) was fire-and-forget. Config/preferences: 316 consecutive AI messages without my input. Holidays widget: 267. StataAPI: 159. Baby monitoring: 120. I'd prompt something, go make dinner, come back to a completed feature with tests passing and a CI deploy queued.
The desktop (home-app) was the coordination hub. I launched parallel sub-agents for CPU-intensive work: Playwright capturing screenshots, the Android emulator running widget tests, multiple feature agents working simultaneously. This is where the agentic spec paid off. Agents knew their roles, their model selection, their boundaries.
The homelab stayed high-ceremony. Every Caddy config change, every Zitadel tweak, every DNS record. All through the relay. Slow but controlled.
The "sleep 5 minutes" pattern was more of a hack to stop Claude from stomping over CD jobs that I wanted to complete, or working around the 5-hour session limit. It wasn't really about autonomous repair — it was about pacing.
The Features
Honestly, I could release several of these as stand-alone apps. Each widget is its own product. Here's what made them interesting:
Transit: The old Dashy setup proxied BVG transit data through Node.js. The new version hits the BVG API directly with a background cache heater that keeps data not just cached but fresh. It's aware of where I was and what bus stations are near me, refreshing those stops continuously. 50ms loads vs. the laggy experience you're used to with usual travel apps. Location-aware, always warm.
Weather: Not just temperature and rain. UV index, sunglasses advisory, asthma triggers, live radar. Information-dense by design. I wanted to glance at my phone and know whether to pack an umbrella, wear sunscreen, or warn my wife about high pollen. All data from DWD (German Weather Service), no third-party intermediaries.
Baby monitoring: Probabilistic curves for feeding and sleep predictions. Trend detection. I looked into using a dataset of 300+ infants to improve the model, but the agency who provides it wanted proof of HIPAA training, and I decided the paperwork wasn't worth it.
Portfolio: Alpha and beta against benchmark index, sparkline charts, holdings breakdown. Data pulled directly from Ghostfolio's API.
Exchange rates: ECB official rates with smart cache warming: starts polling at 16:00 CET on weekdays, retries every 5 minutes until the daily rate publishes.
Baby, weather, portfolio, transit. All awesome. The weather widget gets the most daily use — it's on the homescreen.
The Asymmetric Bets
The time data reveals something about how I use AI for unfamiliar territory:
| Area | Brian's Time | Claude's Time | Ratio |
|---|---|---|---|
| StataAPI (Rust, familiar) | 1h 32m | 4h 57m | 1:3.2 |
| StataApp (Android, zero experience) | 4h 59m | 9h 41m | 1:1.9 |
| Architecture & Planning | 1h 46m | 6h 47m | 1:3.8 |
| Onboarding Wizard | 4h 35m | 6h 10m | 1:1.3 |
The things I knew least about got disproportionate AI investment. Android development (Kotlin, Compose, Glance widgets) was entirely new to me. I'd never built a mobile app. The 5h/10h split means I spent about one hour per widget concept, and Claude handled the Kotlin implementation, the Gradle configuration, the Glance layout constraints, all of it.
Planning shows the inverse pattern: 1h 46m of my time produced 6h 47m of AI execution. The specs were force multipliers.
I have no idea how to program Android or Kotlin. I gave a general look at the code and it was "good code" — but I'm not experienced enough to tell if it's "good Android code." I can leverage my deep programming experience on backend and frontend to tell if code is generally well-structured. It seemed fine.
There were, however, regular issues where Claude wasn't adopting community best practices — which led to a lot of annoying bugs: data refresh not working, Glance widgets not rendering, things hidden from view, auth token issues. It felt like Claude was trying to write code instead of write correct code. I had to redirect it to research and adopt best practices, and after that things moved much smoother.
Phase 4: Polish and Onboarding | Mar 22-23 | Sessions 26-30 | The luxury onboarding wizard, wife onboards, bug blitz
The Luxury Nobody Asked For
At some point I decided the onboarding experience should be absurdly good. Four languages: English, German, Polish, Hungarian, because my family spans four countries. A step-by-step wizard: join the VPN, install F-Droid, create your Zitadel account, discover your apps. Video recordings with click annotations showing exactly where to tap.
Claude recorded the onboarding videos autonomously, driving Playwright to capture screen recordings of each step, overlaying click annotations, converting with ffmpeg. I reviewed the output and picked the good takes.
I replaced the generated emoji icons with 2-char monograms. Emojis are AI-kitsch at this point — everyone sees them and thinks "ugh, more AI slop." It was annoying to me even before AI, honestly.
The Real Test
I ran the full onboarding flow through the Android emulator end-to-end, then had my wife go through it on real hardware.
Eight bugs surfaced during install. Claude fixed them with minimal oversight. The tmux server at work. For the public-facing case study and marketing page, I made sure screenshots used example.com instead of the real family domain, and fake names instead of real family members.
She likes it. She needs to get used to all the new UI, but she uses it daily — and I know she's using it because if she gets logged out unexpectedly when I change something in Zitadel, she tells me.
The Agent Waves
What This Shows
Infrastructure work wasn't harder, it was slower. Auth and Caddy had the highest message counts (453 and 427) despite the lowest error rates (0.1 per message). The messages weren't fixing bugs, they were the cost of operating through a human relay: Claude writes the command, I evaluate it, I press enter, I paste the output back. This is a real constraint of AI-assisted operations. Claude can write a perfect Caddyfile, but someone still has to systemctl reload caddy on the production host.
1 hour 46 minutes of planning produced 6 hours 47 minutes of AI execution. But it's not just the ratio, it's what the specs enabled. The autonomous chains (100-316 consecutive AI messages) happened on the tmux server, where Claude had file access and could run tests. Those chains were only possible because the specs defined the plugin architecture, the data contracts, the error handling patterns, and the testing expectations. Without the specs, autonomy would have been chaos.
The areas where I had the least experience got the most AI leverage. Android development: zero prior experience, 5 hours of my time, 10 hours of Claude's. StataAPI in Rust: familiar territory, but the plugin architecture meant Claude could implement 17 plugins while I designed the host interface. The pattern isn't "AI does the hard parts." It's "AI does the parts where my time-to-competence would be highest."
I didn't plan the domain migration at the start. I didn't plan the luxury onboarding wizard. I didn't plan canvas-based weathergraphs or probabilistic baby feeding curves. Each of these emerged after a previous layer worked. The domain migration happened because Zitadel was stable. The onboarding happened because the app was ready. The prediction curves happened because the Baby Buddy integration worked. Scope expanded not from feature creep but from earned confidence. Each working layer made the next ambitious thing seem achievable.
No topic exceeded 1.5 errors per message. Auth: 0.1. Caddy: 0.1. Weather: 0.1. Transit: 0.1. The challenge was never a single hard problem. It was breadth: approximately 20 concurrent feature surfaces, each with their own data source, API contract, caching strategy, widget layout, and deployment path. Managing that breadth, across three workspaces, four languages, and 18 upstream services, is where the agentic approach and the spec-driven architecture paid off. No individual piece was beyond what a competent developer could build manually. The system as a whole, in three weeks, would not have existed without this workflow.
A thread running through the whole project: every data source is official and direct. Exchange rates from the ECB. Weather from DWD. Transit from BVG. No third-party aggregators, no ad-supported APIs, no data brokers. Family photos behind an encrypted Tailnet VPN on self-hosted Immich, not iCloud or Google Photos.
This is what digital sovereignty looks like. Free to use computers without someone looking to harvest my digital life for power and profit. And I can extend that to my entire family.
Now that I have so much infrastructure in place, extending is even easier. I can add an Element server for cross-family private chats that don't get harvested by Meta. A shared family wiki with private documents that don't sit on a Google-owned server somewhere. The foundation is built. Every new service is just another container behind the same auth, the same VPN, the same family domain.
Build Cost Comparison
A friend I showed this to thought I could sell it. I'm not convinced it's a business. It's a desirable product, but perhaps not profitable. Ultra-high-net-worth individuals spend millions to get custom apps developed for their extended families — if you scoped that concept down to my life, this app fits. I think I'm at the lower end of people who could pay a profitable business for this kind of service. I'd have to target people with 8-digit net worth willing to pay over 10k a year once you factor in hosting, around-the-clock support, and business operations. Possible but challenging. The 90% of people who could benefit but not afford 10k/year would need to be a programmer with deep experience who's willing to adopt tech support for their entire family. Not something for many.
This isn't about high engagement or mass market adoption. So much tech is geared towards that. This is about focused life improvement in small ways with high quality for the people closest to me.