Catchment
Replaces my dad’s handwritten rain log, and grows into managing the home’s tanks, borehole and irrigation.
Phases
- Phase 1 Build
- Phase 2 Pipeline and packaging
- Phase 3 Deploy and network
- Phase 4 Break it
- Phase 5 Monitor and detect
- Phase 6 Move to k3s
Overview
A rain log my dad can use from his phone, built so it can grow into running the water system.
My dad has written down the rainfall every morning since 2009. The notebook works, but nobody can search it, and it can’t tell you whether the tanks will last through a dry month.
Catchment replaces the notebook first. Later phases add tank levels, the borehole pump and irrigation. It’s built in phases so each one ships something he actually uses.
Architecture
One Go binary and one SQLite file. Nothing to keep running except the service itself.
The service uses Go’s standard library net/http with no web framework. Pages
are Go templates, and htmx swaps in partial updates, so there’s no
JavaScript build step1. The rain gauge posts readings to a small ingest API
with a per-device token. A scheduled job stores Open-Meteo forecasts so I can
compare forecast with measured rain later.
Security
The pipeline fails the build on known vulnerabilities, before anything reaches the house.
Threat model. The data is low value. What matters is the pump relay that later phases control: a stuck-on borehole pump runs dry and burns out. So the ingest API is the main attack surface, and every device has its own token that can be revoked on its own2.
What the pipeline scans for. Every push runs the tests, govulncheck for
known vulnerabilities in dependencies, gosec for insecure code patterns, and a
scan of the final container image3.
What it found, and what I fixed. The first gosec run flagged
4 issues. 3 were real: a file read built from user
input, an HTTP server with no timeouts, and a weak random source for tokens. I
fixed all three. The fourth was a false positive, which I suppressed with a
comment explaining why4.
Decisions
Each decision is written down when it’s made, with the reason.
- SQLite, not Postgres. One household, one writer. A single file is easy to back up, and there’s no database server to secure5.
- htmx, not a JavaScript framework. The dashboard is forms and tables. Server rendering keeps all the logic in Go, where the tests are.
- Standard library only for HTTP. Go 1.22’s routing patterns cover
everything, and fewer dependencies means less for
govulncheckto flag.
What broke
The things I got wrong, and what I’d change.
- Time zones. Readings were stored in UTC but my dad logs “this morning”. A reading at 07:00 SAST landed on the previous day. Now the logbook stores the local date he means, not a timestamp.
- Forecast job on restart. The job ran on a timer in memory, so every restart fetched the forecast again. It now checks the last stored snapshot first.
- What I’d change: I’d write the migration tooling before the first schema, not after the second.
Infrastructure
Phase 3 moves Catchment onto its own network segment, behind the same firewall as the rest of the homelab.
The homelab runs Proxmox on a Dell OptiPlex 7020, with pfSense as a VM doing the routing and firewalling. A TP-Link TL-SG105E managed switch carries the VLANs. The attack range is isolated, and nothing in it can reach the rest of the network.
Phase 3 (planned for Aug–Sep 2027) gives Catchment its own VLAN. The firewall only allows HTTPS in from the home network, and only the forecast API out. That phase also adds TLS and nightly backups of the SQLite file.