A lot of what keeps this house running is software nobody thinks about until it breaks. Home Assistant is where the alerts live, which is fine until the power or the internet goes out, and then it goes quiet at exactly the moment I'd want to hear from it. [3DPO](/projects/3dpo/) and [Todos](/projects/todos/) back up their databases every night, and nothing checks that a backup would actually restore. The family photos live on the NAS, with no copy outside the house. And everything we own that isn't software is written down in two Markdown files: a register of tools, parts and vehicles, and a catalog of old video games. Homestack is four apps to deal with that, and two repos that hold them together: - **Sub-Etha** pages my phone, from the cloud, so it still works when the house doesn't. - **Towel** takes the NAS off-site and proves, on a schedule, that it would come back. - **Librarian** knows everything we own and where it is, and is run mostly by talking to Claude. - **Cartographer** knows the shape of every game in the collection and generates 3D-printable racks to fit them. - **Heart of Gold** is the machine they run on and the repo that configures it. **Guide** is the shared Go code that makes four apps behave like one. Four of the names are from *The Hitchhiker's Guide to the Galaxy* and one is from *Snow Crash*. Cartographer maps carts to places. It's early. The repositories were created on October 19, and by that evening the foundations were in: DNS, a web server, a private network, three empty databases and a deploy pipeline that rolls itself back. None of the four apps does anything yet, so this page is mostly the plan. It'll fill in as the pieces land, and there's a log at the bottom. ## One of everything The rule for the whole stack is one VM, one Postgres, one private network, one domain and one set of conventions. The VM is the small one that already runs 3DPO's and Todos' Postgres, with 2 GB of memory, so everything new is a single static Go binary under systemd. The internet sees a few routes and a mail port; every page with a button on it is on the tailnet, reachable from my phone and laptop through [Tailscale](https://tailscale.com/) and from nowhere else. The plan names three flows that carry most of the value, and every piece of work has to move one of them forward: ```text producers → Sub-Etha → phone alerts Claude → Librarian, Cartographer → Postgres intelligence beside the data catalog → generator → 3DPO → printer atoms from bits ``` The middle one is Todos' best idea again: no model runs on the server. The apps hold the data and hand it, photos included, to whichever Claude I'm already talking to. ## Sub-Etha: a pager for the house Home Assistant can't reach me when I'm away, and it goes dark when the house does. Sub-Etha runs on the VM instead. Anything that can make a web request, publish to MQTT or send an email can tell it something, and every event says how bad it is: - **P0, act now.** A water leak, a warm freezer, the UPS on battery. Through Do Not Disturb, every two minutes. If nobody acks it in ten, my wife gets an email. - **P1, act today.** A broken backup, a machine gone missing. Loud, and louder after four hours. - **P2, good to know.** A print finished. Once, quietly. - **P3, digest only.** Backups that worked. A 07:00 email. Something that's broken opens an incident, keyed like `chain/pg-3dpo/restore`, so the fiftieth report of the same problem doesn't page fifty times. Pages go to a self-hosted [ntfy](https://ntfy.sh/) on my phone, each with an **Ack** button: a signed link that can ack that one incident while it's open and do nothing else, so it works from a notification with no login. - **A P0 can't be silenced.** Silences are for planned noise, like a NAS update, and the config check refuses one that would cover a P0. - **The digest always arrives.** On a quiet day it says nothing happened, so a missing digest means the bus is broken. - **It watches itself.** Every minute it pings [healthchecks.io](https://healthchecks.io/), which emails me if the pings stop, and it skips the ping if its last message to my phone failed. - **Spam costs one connection.** The mail port takes only addresses with a per-source secret in them and refuses everything else before the message is sent. Inside, every future action, from a repeat to tomorrow's digest, is a row in one table instead of a timer in memory, so a restart just resumes. Every handler has to be safe to run twice, and its test runs it twice. ### Backups that have to prove it The first and strictest producer is backup attestation. A heartbeat expects a check-in (the ADS-B Pi every five minutes, or else). A probe goes and looks (the newest file in a bucket is recent and the usual size). And a chain follows a nightly backup through its stages, each one posting a receipt with its numbers: ```yaml - name: pg-3dpo kind: chain severity: P1 stages: - id: dump every: 24h grace: 2h expect: bytes: {ratio_min: 0.7, of: median_14} # not shrinking - id: copy within: 2h match: [sha256] # same bytes as the dump - id: restore within: 24h expect: rows: {ratio_min: 0.95, of: previous} # vs last restore max_age_h: {max: 36} # newest restored row ``` A backup that runs, succeeds and quietly comes out empty fails the first stage. A restore of data that stopped changing fails the last. A new check skips the comparisons until it has three receipts, so nothing pages on its first night. The spec follows one failure all the way through. The NAS's restore job never posts its receipt. At the deadline, Sub-Etha opens a P1. My phone buzzes, and over MQTT, Home Assistant lights the LED bar on my office light switch. I tap Ack, which cancels the four-hour escalation. The next good restore resolves it, and the light goes out. **Where it stands:** being built. The first slice is claimed: events in, incidents, the phone, the Ack button, and a `kill -9` that has to get me an email from healthchecks.io within five minutes. ## Towel: the NAS, off-site The NAS holds the photos in [Immich](https://immich.app/), documents, and the configuration for Home Assistant and everything around it. The threats are the usual three: the house burns down, the NAS dies or gets ransomware, or a folder gets deleted and nobody notices for six months. Towel is mostly not software. It's a decision, a sort into tiers, a long upload and a schedule of restore drills, with a small Go program on the NAS that drives [restic](https://restic.net/) and reports to Sub-Etha. What can't be replaced goes nightly and is kept for years; what would just be tedious to redo, like the ripped music, goes weekly; movies and TV don't go at all. The lean is [Backblaze B2](https://www.backblaze.com/cloud-storage), and the deciding number is what a restore costs. Google's colder storage is cheaper to keep but costs about $140 a terabyte to read back in full. B2 is $6.95 a terabyte a month, and reading it back is free up to three times what's stored. A backup I mean to test-restore every quarter shouldn't charge me by the gigabyte to do it. - **The key never lives only on the NAS.** It goes in the password manager and on paper somewhere that won't burn with the house. - **The NAS can't destroy history.** Its credentials can write but never delete. Pruning runs from the VM, so a ransomed NAS can't take the off-site copy down with it. - **Restores are proven.** Monthly, restic reads back a random 5% of the data; quarterly, a drill restores a sample of photos to the laptop and compares hashes. A skipped drill pages me. - **The runbook comes first.** One page, written for "the house is gone and I have a phone." Its test is restoring one file from the laptop using nothing but that page. Its first job runs the other way: pull last night's 3DPO and Todos backups down to the NAS, check the hashes, restore them into a throwaway Postgres, count the rows, and post the `restore` receipt in the chain above. **Where it stands:** a scaffold and an outline of the runbook. Step one is a `du` of every share, which sets both the bill and how long the first upload takes. ## Librarian: where is it? Our stuff is spread across three sites, and the inventory is a Markdown file with numbered rows. #5 is the Corvette. Each row says what's to become of the thing (keep it at the house, keep it at the garage, give it away, sell it, undecided), because there's a consolidation underway. The file is fine at "do we have one." It's bad at "where exactly," "do we have two," "what's waiting to move" and "who borrowed it." Librarian is that file as a database, and like Todos it's MCP-first: the main way in is a conversation with Claude, under Todos' rules, so whatever Claude files waits for review beside the words it came from. The tools get built before the web pages, so the pages can follow how I actually use it instead of how I guessed I would. The part I'm looking forward to is photos. Cataloging a shelf by typing is miserable; photographing it is easy. So the phone uploads a few shots as an *intake batch*. Later I say "catalog batch 17," and Claude calls `get_intake_batch(17)`, which returns the photos themselves, as images in the conversation. Claude works out what's in them, asks about anything it isn't sure of, and files it all in one call, each item pointing at its crop of a photo. Librarian never calls a vision API. The slice that builds this has my favorite test in the plan: a shelf at the other site photographed in four shots and cataloged from the car on the drive home. Under the conversation is a tree of sites, rooms, shelves, bins and hooks, and every item is in exactly one of them. Vehicles are items that are also containers, so the Corvette's parts can live in the Corvette. Every container gets a QR label with a six-character code. The code is its identity and the location is just a property, so a bin that moves keeps its label. Scanning a bin arms **move here**, and every scan after that is an item moving in, committed together: twenty things into a bin is twenty beeps and two taps. Where a sticker would peel, like the pegboard, the label can be a 3D-printed plate with the QR in the geometry, sent to 3DPO like any other print. Nothing is ever deleted: things leave as GONE with a note saying where they went. And status is derived, as in 3DPO and Todos: an item headed for the garage is STAGED until it's put in a container there, and then it's SETTLED on its own. Marking something to sell can file a Todos task, and an overdue loan gets me a P2 the next morning. The spec's definition of done is one sentence: "where is the tap extractor set," asked in Claude, answered in one tool call. **Where it stands:** a scaffold. First comes an importer that has to reproduce every number in the Markdown file, then the tools, whose test is "where is the Corvette's carburetor" answered in one call. ## Cartographer: a slot for every cartridge The game collection is about 169 items across 12 platforms, plus 15 consoles, in the other Markdown file. I want it on the shelf in printed organizers sized for exactly what we own plus room to grow, and I want those organizers to come out of the catalog, not be a project I redo every time I buy a game. So Cartographer knows the physical shape of every copy. A title is the game; a copy is the cartridge on the shelf, with a form factor (`snes-cart`, `dvd-case`, `nes-box`) and, eventually, a slot. Duplicates are just more copies. The starting dimensions come from a community table, and the sources don't always agree: two of them put the N64 cartridge at 76.6 mm and 89 mm tall. So nothing renders for a form factor nobody has measured with calipers, and the first step of the build has no code in it. Then comes a coupon, a five-slot test strip with a different clearance embossed on each slot. The one a cartridge slides into without rattling, and stays in when the strip is tipped 30 degrees, is accepted with `carto coupon --accept snes-cart 0.6`, and only then does SNES unlock for full renders. No filament goes into a rack sized from somebody else's numbers. Three kinds of organizer cover the collection: upright racks for cartridges labeled on top (SNES, N64, Genesis) and cases with spines; stepped trays, whose rows rise and step back so front labels (NES, Game Boy) are visible from where you sit; and case racks that grip boxed games only along the bottom so nothing gets crushed. Sonic & Knuckles, which is bigger than everything else, gets a wider slot of its own. The organizers are code. Go fills in parameters, a template turns them into [OpenSCAD](https://openscad.org/), and OpenSCAD renders it. One command runs the whole pipeline: ```text carto gen --family upright --formfactor snes-cart --shelf 600x250 ``` It picks the copies in title order, adds 20% spare slots, fits them to the shelf, and splits anything longer than the printer's bed into segments joined by dovetails, with the seams between slots, never through one. Then it renders, writes a print plan for 3DPO, and records which slot every copy landed in. Same catalog, same parameters, same OpenSCAD version: byte-identical files. Photo intake works the way it does in Librarian, starting with the handheld cartridges, which have never been cataloged. Every night Cartographer tells Librarian where each copy is, so "where is Mario Kart 64 copy 2" gets an answer from Librarian: the shelf, the organizer, the slot. And eventually there's a public export: a page on this site listing the collection, without values or locations. **Where it stands:** a scaffold. The next step is calipers. ## The machine and the glue **Heart of Gold** is the VM and the repo that configures it: the web server ([Caddy](https://caddyserver.com/)), the Tailscale routes, provisioning scripts, runbooks, the specs, and DNS as a YAML file. A script diffs that file against the zone, shows the plan and never deletes anything, and records it doesn't list, like this site's, are never touched. Deploys are one command. `make deploy` builds the binary, copies it over, restarts the service and waits for the health check to report the *new* version, so an old process can't pass for it. If it doesn't, the previous binary, unit and config go back. `make deploy-broken` is the drill: it ships a binary that crashes on purpose and passes only if the rollback is clean. Its first run watched systemd restart the broken one nine times, then put the good one back. **Guide** is where a convention becomes code. Anything three services would otherwise copy lives there: the service skeleton, config that reloads on a signal, Sub-Etha's durable-timer scheduler, the MCP scaffold with Todos' OAuth server, and an OpenSCAD label module, so a pegboard QR plate and an organizer's slot label are the same code with different parameters. The pages are plain server-rendered HTML with [htmx](https://htmx.org/): no build step, no Node, and one forty-line stylesheet shared by all three apps. The shapes the repos agree on, like an event or a backup receipt, are Go types in Guide, and changing one means updating every repo that uses it in the same piece of work. ## Built in slices Like 3DPO and Todos, this is being built by Claude Code, with me as product owner and reviewer. What's new is the size: six repositories, changes that cross them, and possibly more than one session at a time. So the specs came first, written with Claude: one for the conventions and the contracts between repos, and one per app. The unified one breaks the work into 33 slices in five waves. Each slice is a file in the control repo, and its front matter is the board's entire state: ```yaml id: S-25 title: Intake batches (spec step 5) wave: 2 repos: [librarian, heartofgold, guide] depends_on: [S-22, S-03] status: todo ``` A slice is one session's worth of work, and it ends with a test on the real thing, never a mock: unplugging the Pi pages within its grace period; a two-segment SNES rack is printed, joined and loaded, and every copy's slot in the database matches the shelf. - **Claiming is a commit.** A session sets `status: doing` and pushes it before writing any code. The claim is the lock. - **Decisions have a pecking order.** The unified spec beats the app spec, which beats the slice, which beats the code. A session that disagrees with something higher up writes it down and carries on, instead of quietly deciding otherwise. - **Discoveries get written down,** in a dated changes file, with the commands that found them. Nothing in a spec is rewritten without an entry there. - **Some things wait for me.** A session needs my go-ahead in the slice file to delete a DNS record, open a firewall port, touch what 3DPO or Todos use, prune backups or delete anything on the NAS. And it can run only the production commands I've allowed by name: fourteen exact `make` targets, so far. ## What the first day taught me Wave 0 was the foundations, and it took about six hours. A few things didn't match the plan: - **The VM wasn't mine.** It's created by 3DPO's scripts, which rewrite part of it on every boot, and the DNS lives in a different cloud project from everything else. So Heart of Gold is a polite guest: it adds to that VM and never edits what 3DPO put there. - **There were two backup jobs, not one,** twenty minutes apart so they never compete for 2 GB of memory. Rather than edit another app's job, Heart of Gold got a third, twenty minutes after the second. - **The spec was wrong about email.** It pointed a mail record at an alias, which [RFC 2181](https://www.rfc-editor.org/rfc/rfc2181#section-10.3) forbids. The session built it as written, wrote the disagreement into the slice, and changed it once I said yes. That's the pecking order doing its job. - **CI was red on the very first push** in all five Go repos, mostly over unchecked errors in the generated scaffold. - **The first backups were 376, 570 and 373 bytes,** which is what an empty database weighs. ## Where it's going Three tracks run side by side, each ending in something I can use: - **Backups attested:** Sub-Etha's skeleton, heartbeats and escalation, then the backup chains. Done when there are three clean nights of green, and switching off the NAS's restore job gets me a P1 the next morning. - **Librarian from Claude:** the MCP scaffold, the importer, the core tools, then photo intake. - **The first rack:** calipers, coupons, then that two-segment SNES rack. After those, the rest gets picked by appetite: Sub-Etha's link to Home Assistant, its mail port and a "wall of green" dashboard; Librarian's loans and stocktakes; the rest of Cartographer's organizers; Towel in full; and 3DPO reporting real print times back to Cartographer. ## Progress log Newest first. - **October 19.** The repositories, created and green. Wave 0 went in the same day: DNS as code, Caddy with certificates, a tailnet, three databases with their own nightly backup, Guide v0.1.0, and `make deploy` with its rollback drill. One test is left, waiting on the first night's backup. Sub-Etha's first slice is claimed.