A week ago I started Ceres, the multi-agent garden assistant for our raised bed garden. The plan called for soil sensors, a camera, weather data, multi-year history in TimescaleDB, and seven specialist agents producing a daily briefing. Big plan. Still no code.
A week later there is, in fact, code. The data layer landed. So did a careful re-design that ripped out part of it within three days. Most of this update is about that second story.
The schema went in
Phase B of the backlog is “compute and data infrastructure,” and the core of that is the garden TimescaleDB database. I now have:
- Around a dozen relational tables covering beds, plant profiles, plantings, events, feeding events, recommendations, outcomes, and agent runs.
- 4 hypertables originally planned for raw soil moisture, weather observations, bed brightness, and agent call telemetry.
- 4 continuous aggregates rolling those up into hourly and daily summaries.
- Three Postgres roles: agent_rw for the backend, nocodb_rw for spreadsheet UI, grafana_ro for dashboards. Default privileges set so new tables auto-grant the right access without me touching them again.
- An 11-plant seed catalog already loaded with carrots, tomatoes, kale, etc.
The migration was a single Alembic file. Tested locally, committed, pushed.
Then I went to wire up the sensors
The Garden Assist camera is online, a Reolink RLC-510WA connected via WiFi. It’s integrated into Home Assistant and we’ve already seen a rabbit problem we need to address. It’s not doing anything intelligent yet, but it’s ready to go and I’ll be using it to map sun exposure for the beds first. We’ll need some sun to show up for that, but that’s a different problem.

The next step was the bridge: five WH51 soil moisture sensors are paired to my Ecowitt gateway, which feeds Home Assistant, which writes every state change into a homeassistant Postgres database via HA’s built-in recorder. I needed to copy those rows into my soil_moisture_readings hypertable, applying calibration and bed mapping along the way.
I started writing it. Then I stopped. Two problems were sitting in front of me.
First, HA’s recorder keeps about ten days of state by default. Ceres needs multi-year history. So either I reconfigure HA to keep raw forever or I maintain my own full-resolution copy of every state change forever. Neither was good.
Second, and this is the one that decided it: my Tempest weather station is on the same path. Wind, rain, temperature, humidity, UV all flow through HA. So does anything I add later. Every new HA-sourced signal would mean another bespoke bridge writing into another raw hypertable. One bridge per source, forever.
The fix
I changed the design. The rule now:
Home Assistant’s recorder is the source of truth for raw HA-sourced telemetry. The garden database owns calibrated, bed-tagged, aggregated derivatives. Nothing else.
Multi-year history matters because Kim should be able to ask “how did the kale do last May?” three years from now and get a real answer. That only works if the data outlives HA’s ten-day window, and it only stays clean if one system, not two, owns the curated copy.
- The soil_moisture_readings and weather_observations raw hypertables came out. So did the four continuous aggregates that fed them.
- In their place: plain Postgres tables for hourly and daily aggregates, populated by one ingest job, written once with calibration applied and bed-id resolved.
- A new sensor_bindings table maps each HA entity_id to a Ceres-side sensor_id, a bed_id (or a weather source), and a calibration spec stored as JSONB. Linear today; whatever shape I need tomorrow.
- The ingest job reads recent state changes from HA, applies calibration, groups by hourly and daily buckets, and upserts. Run it hourly from n8n. Miss a run? The default three-hour lookback heals on the next one. No backfill tool needed.
Same job covers Ecowitt, Tempest, and whatever HA-routed signal I add next. One ingest pattern. One curated set of tables. Multi-year history without bloating HA.
NocoDB is up
The other thing that happened this week is the NocoDB container is running and reachable behind Nginx Proxy Manager. NocoDB is the spreadsheet-style web UI we will use to manage the garden; beds, plant profiles, plantings, recommendations, the daily journal.
The DB-side prep for that has landed: writes are revoked from nocodb_rw on every infrastructure table (Alembic state, agent telemetry, ingest aggregates, vision outputs), and six curated views now live in the database: Currently Planted by Bed, Recommendations Needing Review, Needs Your Judgment, Year-over-Year Harvests, Recent Journal Entries, Fertilization Due This Week. Building those as Postgres views instead of NocoDB-side filters means the same logic is reusable from Grafana and FastAPI later, and survives any NocoDB rebuild.
Next I point NocoDB at the garden database, build the three input forms, and sit down for a two to three hour data-entry session to populate bed metadata, plant-profile preferences, and what’s currently in the ground. That session is the first foundational deliverable. No agents yet. Just the foundation that everything downstream depends on.
What I learned this week
Two things worth writing down.
The first lesson is that the hardest call in a multi-system build is not technical. It is “who owns this concept.” Once I named HA as the source of truth for raw telemetry and Ceres as the source of truth for curated derivatives, the implementation collapsed to one ingest pattern. The clarification was a product decision before it was a code change.
The second lesson is about sequencing. The original migration sat committed for two days before I tore half of it out. Nothing downstream existed yet, so the cost was zero. The fear of “what if we need to change the schema later” evaporates when you sequence the work so “later” is still cheap. That is why I am building the data layer before the agents, not the other way around.
The product call
The redesign was not a bug fix. It was a decision to spend three days throwing away work because the alternative was a maintenance tax that compounds. One bespoke bridge per HA source, forever, sounds tolerable when you have one source. It is unacceptable by source number five. Recognizing compounding cost before it shows up, and being willing to delete recent work when you spot it, is most of the job.
Up next: finish wiring NocoDB to the garden DB, the data-entry session, and start populating sensor_bindings so the ingest job has actual rows to write.
