<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joao Pedro Bragatti Winckler</title>
    <description>The latest articles on DEV Community by Joao Pedro Bragatti Winckler (@joao_pedrobragattiwinck).</description>
    <link>https://dev.to/joao_pedrobragattiwinck</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3712622%2Fd46cd575-a7ad-4230-b555-d2fac0ae8f92.jpg</url>
      <title>DEV Community: Joao Pedro Bragatti Winckler</title>
      <link>https://dev.to/joao_pedrobragattiwinck</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joao_pedrobragattiwinck"/>
    <language>en</language>
    <item>
      <title>How I Built a Climate-Aware Garden Planner as a Solo Developer</title>
      <dc:creator>Joao Pedro Bragatti Winckler</dc:creator>
      <pubDate>Thu, 15 Jan 2026 11:46:36 +0000</pubDate>
      <link>https://dev.to/joao_pedrobragattiwinck/how-i-built-a-climate-aware-garden-planner-as-a-solo-developer-283</link>
      <guid>https://dev.to/joao_pedrobragattiwinck/how-i-built-a-climate-aware-garden-planner-as-a-solo-developer-283</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: Every Garden Planner Uses Texas Weather
&lt;/h2&gt;

&lt;p&gt;I'm a hobby gardener in the UK, and every spring I'd pull up a garden planning app to figure out when to plant my tomatoes. The app would cheerfully tell me to plant in March.&lt;/p&gt;

&lt;p&gt;March in the UK means frost. Lots of frost. My tomatoes would die.&lt;/p&gt;

&lt;p&gt;The problem? Every garden planner I found used generic USDA hardiness zones or, worse, assumed you lived in California. They'd give you planting dates that worked great in San Diego but were completely wrong for Manchester.&lt;/p&gt;

&lt;p&gt;After killing enough seedlings, I decided to build my own. This is the story of Leaftide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes Garden Planning Hard
&lt;/h2&gt;

&lt;p&gt;Before diving into the technical solution, let me explain why this is actually a complex problem.&lt;/p&gt;

&lt;p&gt;Garden planning isn't just "plant tomatoes in spring." It's a multi-constraint optimization problem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frost dates&lt;/strong&gt; - Plants die if exposed to frost at the wrong stage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thermal time&lt;/strong&gt; - Plants need accumulated heat (Growing Degree Days) to mature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Photoperiod&lt;/strong&gt; - Some plants only flower when day length is right&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temperature ranges&lt;/strong&gt; - Each plant has min/max temps for germination and growth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-year cycles&lt;/strong&gt; - Fruit trees take years to produce, need tracking across seasons&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most garden planners ignore 2-5 entirely. They just use frost dates and call it a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;p&gt;I'm a Django developer by trade, so the choice was obvious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Django (Python)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; HTMX for most interactions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex UI:&lt;/strong&gt; Vanilla JavaScript for the plot designer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment:&lt;/strong&gt; Single VPS, no fancy infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why HTMX?&lt;/strong&gt; I wanted server-side rendering with modern UX. HTMX lets me write Python templates and get SPA-like interactions without a JavaScript framework. For 90% of the app, it's perfect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why vanilla JS for plot designer?&lt;/strong&gt; The plot designer is an SVG-based canvas where users drag plants onto beds. That level of interactivity needs proper state management. I built a custom SVG manipulation layer rather than pulling in React just for one feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Climate-Aware Scheduling Engine
&lt;/h2&gt;

&lt;p&gt;This is the core differentiator. Here's how it works:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Frost Date Calculation
&lt;/h3&gt;

&lt;p&gt;I use NOAA climate data to calculate local frost dates. Not USDA zones (too coarse), actual frost probability curves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_frost_dates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Get historical frost data for location
&lt;/span&gt;    &lt;span class="n"&gt;climate_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_noaa_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Calculate 10%, 50%, 90% frost probability dates
&lt;/span&gt;    &lt;span class="n"&gt;last_spring_frost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;climate_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;first_fall_frost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;climate_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;last_spring_frost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_fall_frost&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users enter their location, I calculate their specific frost dates. No more "Zone 8" nonsense.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Growing Degree Days (GDD)
&lt;/h3&gt;

&lt;p&gt;Plants don't care about calendar dates. They care about accumulated heat.&lt;/p&gt;

&lt;p&gt;Tomatoes need ~1500 GDD to mature. If you plant when it's cold, they'll take forever. If you plant when it's hot, they'll mature faster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_gdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daily_temps&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;gdd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;daily_temps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# GDD = (max + min) / 2 - base_temp
&lt;/span&gt;        &lt;span class="n"&gt;daily_gdd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;base_temp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;gdd&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;daily_gdd&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;gdd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaftide calculates: "If you plant on May 1, you'll accumulate 1500 GDD by July 15." That's your harvest date.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Photoperiod Constraints
&lt;/h3&gt;

&lt;p&gt;Onions are tricky. They only bulb when day length exceeds a threshold (12-16 hours depending on variety).&lt;/p&gt;

&lt;p&gt;Plant too early? They'll grow leaves forever and never bulb.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_daylength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Solar declination calculation
&lt;/span&gt;    &lt;span class="n"&gt;day_of_year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timetuple&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;tm_yday&lt;/span&gt;
    &lt;span class="n"&gt;declination&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;23.45&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;day_of_year&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Hour angle calculation
&lt;/span&gt;    &lt;span class="n"&gt;hour_angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arccos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;tan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;tan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declination&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Daylength in hours
&lt;/span&gt;    &lt;span class="n"&gt;daylength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hour_angle&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;daylength&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leaftide checks: "Will this onion variety get enough daylight to bulb before fall?"&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Putting It Together
&lt;/h3&gt;

&lt;p&gt;The scheduling engine runs all these constraints and returns optimal planting windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_planting_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variety&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_harvest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;constraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;FrostConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frost_tolerance&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;GDDConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gdd_requirement&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;PhotoperiodConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;daylength_sensitivity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;TemperatureConstraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_temp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Work backwards from target harvest
&lt;/span&gt;    &lt;span class="n"&gt;possible_dates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;plant_date&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;date_range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;_satisfied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plant_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;possible_dates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plant_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;possible_dates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users see green dates (optimal), yellow dates (risky), red dates (don't plant).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Permanent Plants Problem
&lt;/h2&gt;

&lt;p&gt;Here's what made me realize this needed to exist: fruit trees.&lt;/p&gt;

&lt;p&gt;I planted an apple tree in 2023. Every garden planner I tried treated it like a tomato plant - "plant it, harvest it, done."&lt;/p&gt;

&lt;p&gt;But fruit trees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Take 3-5 years to produce fruit&lt;/li&gt;
&lt;li&gt;Need annual pruning at specific times&lt;/li&gt;
&lt;li&gt;Have multi-year lifecycle events (bud break, flowering, fruit set)&lt;/li&gt;
&lt;li&gt;Require tracking across seasons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No garden planner handled this. They're all built for annual vegetables.&lt;/p&gt;

&lt;p&gt;So I built permanent plant tracking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PermanentPlant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;variety&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Variety&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;planted_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;age_at_planting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IntegerField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Years old when planted
&lt;/span&gt;
    &lt;span class="c1"&gt;# Lifecycle tracking
&lt;/span&gt;    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FULL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Full lifecycle tracking&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LIBRARY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Reference only&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlantEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;plant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermanentPlant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;BUD_BREAK&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Buds opening&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FLOWERING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Flowers appearing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FRUIT_SET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fruit forming&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HARVEST_START&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;First harvest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;LEAF_FALL&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Leaves dropping&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PRUNING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Pruned&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PEST_SIGHTING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Pest spotted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FERTILIZING&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Fertilizer applied&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Users log events as they happen. The system learns patterns and predicts next year's events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This became the killer feature.&lt;/strong&gt; All 6 of my paid users use permanent plant tracking. None of them care much about the vegetable planning.&lt;/p&gt;

&lt;p&gt;Lesson learned: The feature you think is core might not be what users actually pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plot Designer: When HTMX Isn't Enough
&lt;/h2&gt;

&lt;p&gt;Most of Leaftide uses HTMX. Click a button, server returns HTML, swap it in. Simple.&lt;/p&gt;

&lt;p&gt;But the plot designer needed real interactivity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag beds around a canvas&lt;/li&gt;
&lt;li&gt;Resize placement rectangles&lt;/li&gt;
&lt;li&gt;Real-time spacing calculations&lt;/li&gt;
&lt;li&gt;SVG manipulation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HTMX can't do this. I needed JavaScript.&lt;/p&gt;

&lt;p&gt;I built a custom SVG state manager:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PlotDesigner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;placements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;handleDragStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;startX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;startY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;originalTransform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transform&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;handleDragMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Update SVG transform&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dragState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transform&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="s2"&gt;`translate(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The designer syncs with Django via periodic saves. User drags stuff around in JS, clicks save, JS sends JSON to Django endpoint.&lt;/p&gt;

&lt;p&gt;Hybrid approach: HTMX for 90% of the app, vanilla JS for the 10% that needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Acquisition: Reddit Was Everything
&lt;/h2&gt;

&lt;p&gt;I launched on Product Hunt. Got 50 upvotes. Zero conversions.&lt;/p&gt;

&lt;p&gt;Then I posted on r/BackyardOrchard about the permanent plant tracking feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3 paid users in 24 hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turns out, people searching for "garden planner" want free tools. People in r/BackyardOrchard have $500 fruit trees and need to track them.&lt;/p&gt;

&lt;p&gt;All 6 of my paid users came from Reddit. Not from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product Hunt&lt;/li&gt;
&lt;li&gt;Google Ads (tried it)&lt;/li&gt;
&lt;li&gt;SEO (still building)&lt;/li&gt;
&lt;li&gt;Twitter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson: Find where your actual users hang out. For me, it's niche gardening subreddits, not generic startup communities.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Climate Data is Messy
&lt;/h3&gt;

&lt;p&gt;NOAA data comes in weird formats. CSV files with inconsistent column names. Missing data for some locations. Stations that moved.&lt;/p&gt;

&lt;p&gt;I spent weeks cleaning and normalizing it. Built a pipeline to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download raw NOAA data&lt;/li&gt;
&lt;li&gt;Interpolate missing values&lt;/li&gt;
&lt;li&gt;Calculate frost probabilities&lt;/li&gt;
&lt;li&gt;Cache results per location&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not glamorous, but essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Multi-Tenancy Without Going Crazy
&lt;/h3&gt;

&lt;p&gt;Every user has their own garden, plants, and data. I needed proper isolation.&lt;/p&gt;

&lt;p&gt;Django doesn't have built-in multi-tenancy. I built a middleware that sets a thread-local user context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurrentUserMiddleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="ow"&gt;is&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;_authenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;set_current_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;clear_current_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then every model query automatically filters by current user. No accidental data leaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: Performance With Complex Calculations
&lt;/h3&gt;

&lt;p&gt;Calculating planting windows for 100+ varieties across 365 days = expensive.&lt;/p&gt;

&lt;p&gt;I cache aggressively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frost dates per location (rarely change)&lt;/li&gt;
&lt;li&gt;GDD accumulation curves (pre-calculate for common dates)&lt;/li&gt;
&lt;li&gt;Planting windows per variety (invalidate when user changes location)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: Page loads in &amp;lt;200ms even with complex calculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monetization: Freemium That Actually Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Free tier:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 seasonal varieties&lt;/li&gt;
&lt;li&gt;10 permanent plants&lt;/li&gt;
&lt;li&gt;2 custom varieties&lt;/li&gt;
&lt;li&gt;Basic plot designer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro tier (£5/month or £45/year):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unlimited everything&lt;/li&gt;
&lt;li&gt;Advanced plot designer features&lt;/li&gt;
&lt;li&gt;Priority support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key: Free tier is genuinely useful. You can plan a real garden. But serious gardeners hit limits fast.&lt;/p&gt;

&lt;p&gt;Conversion rate: ~12% of signups try Pro (7-day trial). About 50% of those convert to paid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Build for a Real Problem You Have
&lt;/h3&gt;

&lt;p&gt;I didn't start with market research or competitor analysis. I started with: "Why can't I find a garden planner that knows when to plant tomatoes in Scotland?"&lt;/p&gt;

&lt;p&gt;This gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentic understanding&lt;/strong&gt; of the problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in validation&lt;/strong&gt; (if I need it, others do too)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motivation to finish&lt;/strong&gt; (I actually wanted to use it)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Reddit &amp;gt; Product Hunt (For Niche Products)
&lt;/h3&gt;

&lt;p&gt;Product Hunt got me 500 visitors and 0 conversions. Reddit got me all 6 paid users.&lt;/p&gt;

&lt;p&gt;Why? Product Hunt users are browsing for cool products. Reddit users are searching for solutions to specific problems.&lt;/p&gt;

&lt;p&gt;Find where your users are already asking questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Feature That Converts Isn't Always What You Think
&lt;/h3&gt;

&lt;p&gt;I thought the climate-aware scheduling would be the killer feature. It's technically impressive and solves a real problem.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;permanent plants tracking&lt;/strong&gt; is what converts. Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduling is a one-time lookup ("when do I plant tomatoes?")&lt;/li&gt;
&lt;li&gt;Permanent plants are ongoing tracking ("when do I prune my apple tree next year?")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People pay for tools they'll use repeatedly, not one-time lookups.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. HTMX Is Great Until It Isn't
&lt;/h3&gt;

&lt;p&gt;HTMX let me build 90% of the app without writing JavaScript. But when I hit that 10% (the plot designer), I had to write vanilla JS anyway.&lt;/p&gt;

&lt;p&gt;The lesson: &lt;strong&gt;Use the right tool for each job.&lt;/strong&gt; Don't force a paradigm where it doesn't fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Solo Dev Means Ruthless Prioritization
&lt;/h3&gt;

&lt;p&gt;I have a backlog of 50+ feature ideas. I've shipped maybe 10.&lt;/p&gt;

&lt;p&gt;The difference between a side project and a business is saying no to good ideas so you can focus on great ones.&lt;/p&gt;

&lt;p&gt;Every hour spent on a feature is an hour not spent on marketing.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Freemium Works If the Free Tier Is Generous
&lt;/h3&gt;

&lt;p&gt;My free tier gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 seasonal varieties&lt;/li&gt;
&lt;li&gt;10 permanent plants&lt;/li&gt;
&lt;li&gt;2 custom varieties&lt;/li&gt;
&lt;li&gt;Full plot designer&lt;/li&gt;
&lt;li&gt;All scheduling features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's enough to actually use the app. Users upgrade when they hit limits, not because the free tier is crippled.&lt;/p&gt;

&lt;p&gt;12% conversion rate suggests this works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Leaftide is live at &lt;a href="http://leaftide.com" rel="noopener noreferrer"&gt;leaftide.com&lt;/a&gt;. It's a real product with real paying users, but it's still early.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current status:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6 paid users (all from Reddit)&lt;/li&gt;
&lt;li&gt;£30/month MRR&lt;/li&gt;
&lt;li&gt;Solo developer&lt;/li&gt;
&lt;li&gt;Launched October 2025&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I'm working on:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;More permanent plants&lt;/strong&gt; - The feature that converts needs more varieties (currently ~50, targeting 200+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile app&lt;/strong&gt; - Android app in testing, iOS planned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better onboarding&lt;/strong&gt; - Too many users sign up and don't add their first plant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community features&lt;/strong&gt; - Users want to share their gardens and learn from each other&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The biggest challenge?&lt;/strong&gt; Marketing. Building the product was the easy part. Finding users who need it is the hard part.&lt;/p&gt;

&lt;p&gt;If you're building a niche product, my advice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build something you actually need&lt;/li&gt;
&lt;li&gt;Find where your users already hang out (not Product Hunt)&lt;/li&gt;
&lt;li&gt;Focus on features that create ongoing value, not one-time lookups&lt;/li&gt;
&lt;li&gt;Ship fast, iterate based on real user feedback&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Want to try it?&lt;/strong&gt; Check out &lt;a href="http://leaftide.com" rel="noopener noreferrer"&gt;leaftide.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions about the tech?&lt;/strong&gt; Drop them in the comments. I'm happy to dive deeper into any part of the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building something similar?&lt;/strong&gt; I'd love to hear about it. Climate-aware software is an underserved space.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>django</category>
      <category>htmx</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
