In progress

Catchment

Replaces my dad’s handwritten rain log, and grows into managing the home’s tanks, borehole and irrigation.

Started Nov 2026 · updated Jun 2027 Go · SQLite · htmx · Docker · GitHub Actions

Phases

  1. Phase 1 Build Shipped · Mar 2027
  2. Phase 2 Pipeline and packaging Shipped · May 2027
  3. Phase 3 Deploy and network Planned · Aug–Sep 2027
  4. Phase 4 Break it Planned · Q4 2027
  5. Phase 5 Monitor and detect Planned · 2028
  6. Phase 6 Move to k3s Planned · 2028

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.

Catchment architecture A browser, a rain gauge device and the Open-Meteo forecast API connect to one Go service with three parts: the dashboard and rain logbook, the device ingest API, and a forecast snapshot job. The service stores everything in one SQLite file. A GitHub Actions pipeline runs tests, govulncheck, gosec and an image scan, then builds the Docker image. Browser htmx dashboard Rain gauge device, HTTPS POST Open-Meteo forecast API Go service stdlib net/http Dashboard and rain logbook Go templates + htmx Device ingest API per-device token, JSON Forecast snapshots scheduled job SQLite one file, WAL mode GitHub Actions tests → govulncheck → gosec → image scan → Docker image Catchment architecture A browser, a rain gauge device and the Open-Meteo forecast API connect to one Go service with three parts: the dashboard and rain logbook, the device ingest API, and a forecast snapshot job. It stores everything in one SQLite file. A GitHub Actions pipeline scans and builds the Docker image. Browser htmx dashboard Rain gauge device Open-Meteo forecast API Go service stdlib net/http Dashboard and logbook Go templates + htmx Device ingest API per-device token Forecast snapshots scheduled job SQLite one file, WAL mode GitHub Actions tests → govulncheck → gosec → image scan → Docker image
Catchment as of Phase 2: one Go binary, one SQLite file.

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 govulncheck to 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.

Home network layout The internet connects to a pfSense firewall running as a VM on Proxmox. pfSense connects to a TP-Link TL-SG105E managed switch, which carries four VLANs: a Wi-Fi target network, a management VLAN for Proxmox and Wazuh, an isolated attack range, and a Catchment VLAN that is planned for Phase 3. Internet pfSense firewall + router VM on Proxmox TL-SG105E managed switch Wi-Fi target Wi-Fi exercises Management Proxmox, Wazuh Attack range isolated, no route out Catchment planned, Phase 3 Home network layout Internet, then a pfSense firewall VM on Proxmox, then a TL-SG105E managed switch carrying four VLANs: Wi-Fi target, management (Proxmox and Wazuh), an isolated attack range, and a planned Catchment VLAN. Internet pfSense firewall + router, VM on Proxmox TL-SG105E managed switch, four VLANs Wi-Fi target Wi-Fi exercises Management Proxmox, Wazuh Attack range isolated, no route out Catchment planned, Phase 3
Dashed means planned. Catchment moves onto its own VLAN in Phase 3.

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.

Footnotes

  1. Placeholder evidence link: cmd/catchment/main.go in the repo. ↩

  2. Placeholder: docs/decisions/0004-per-device-tokens.md. ↩

  3. Placeholder: .github/workflows/ci.yml. ↩

  4. Placeholder: commit fixing the gosec findings. See also the pipeline write-up. ↩

  5. Placeholder: docs/decisions/0001-sqlite.md. ↩