DEV Community

Joao Pedro Bragatti Winckler
Joao Pedro Bragatti Winckler

Posted on

How I Built a Climate-Aware Garden Planner as a Solo Developer

The Problem: Every Garden Planner Uses Texas Weather

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.

March in the UK means frost. Lots of frost. My tomatoes would die.

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.

After killing enough seedlings, I decided to build my own. This is the story of Leaftide.

What Makes Garden Planning Hard

Before diving into the technical solution, let me explain why this is actually a complex problem.

Garden planning isn't just "plant tomatoes in spring." It's a multi-constraint optimization problem:

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

Most garden planners ignore 2-5 entirely. They just use frost dates and call it a day.

The Tech Stack

I'm a Django developer by trade, so the choice was obvious:

  • Backend: Django (Python)
  • Frontend: HTMX for most interactions
  • Complex UI: Vanilla JavaScript for the plot designer
  • Database: PostgreSQL
  • Deployment: Single VPS, no fancy infrastructure

Why HTMX? 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.

Why vanilla JS for plot designer? 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.

The Climate-Aware Scheduling Engine

This is the core differentiator. Here's how it works:

1. Frost Date Calculation

I use NOAA climate data to calculate local frost dates. Not USDA zones (too coarse), actual frost probability curves.

def calculate_frost_dates(latitude, longitude):
    # Get historical frost data for location
    climate_data = fetch_noaa_data(latitude, longitude)

    # Calculate 10%, 50%, 90% frost probability dates
    last_spring_frost = calculate_percentile(climate_data, 0.1)
    first_fall_frost = calculate_percentile(climate_data, 0.9)

    return last_spring_frost, first_fall_frost
Enter fullscreen mode Exit fullscreen mode

Users enter their location, I calculate their specific frost dates. No more "Zone 8" nonsense.

2. Growing Degree Days (GDD)

Plants don't care about calendar dates. They care about accumulated heat.

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.

def calculate_gdd(base_temp, max_temp, daily_temps):
    gdd = 0
    for temp in daily_temps:
        # GDD = (max + min) / 2 - base_temp
        daily_gdd = max(0, (temp - base_temp))
        gdd += daily_gdd
    return gdd
Enter fullscreen mode Exit fullscreen mode

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

3. Photoperiod Constraints

Onions are tricky. They only bulb when day length exceeds a threshold (12-16 hours depending on variety).

Plant too early? They'll grow leaves forever and never bulb.

def calculate_daylength(latitude, date):
    # Solar declination calculation
    day_of_year = date.timetuple().tm_yday
    declination = 23.45 * sin(360/365 * (day_of_year - 81))

    # Hour angle calculation
    hour_angle = arccos(-tan(latitude) * tan(declination))

    # Daylength in hours
    daylength = 2 * hour_angle / 15
    return daylength
Enter fullscreen mode Exit fullscreen mode

Leaftide checks: "Will this onion variety get enough daylight to bulb before fall?"

4. Putting It Together

The scheduling engine runs all these constraints and returns optimal planting windows:

def calculate_planting_window(variety, location, target_harvest):
    constraints = [
        FrostConstraint(variety.frost_tolerance),
        GDDConstraint(variety.gdd_requirement),
        PhotoperiodConstraint(variety.daylength_sensitivity),
        TemperatureConstraint(variety.min_temp, variety.max_temp)
    ]

    # Work backwards from target harvest
    possible_dates = []
    for plant_date in date_range(start, end):
        if all([c.is](http://c.is)_satisfied(plant_date, location) for c in constraints):
            possible_dates.append(plant_date)

    return possible_dates
Enter fullscreen mode Exit fullscreen mode

Users see green dates (optimal), yellow dates (risky), red dates (don't plant).

The Permanent Plants Problem

Here's what made me realize this needed to exist: fruit trees.

I planted an apple tree in 2023. Every garden planner I tried treated it like a tomato plant - "plant it, harvest it, done."

But fruit trees:

  • Take 3-5 years to produce fruit
  • Need annual pruning at specific times
  • Have multi-year lifecycle events (bud break, flowering, fruit set)
  • Require tracking across seasons

No garden planner handled this. They're all built for annual vegetables.

So I built permanent plant tracking:

class PermanentPlant(models.Model):
    variety = models.ForeignKey(Variety)
    planted_date = models.DateField()
    age_at_planting = models.IntegerField()  # Years old when planted

    # Lifecycle tracking
    mode = models.CharField(choices=[
        ('FULL', 'Full lifecycle tracking'),
        ('LIBRARY', 'Reference only')
    ])

class PlantEvent(models.Model):
    plant = models.ForeignKey(PermanentPlant)
    event_type = models.CharField(choices=[
        ('BUD_BREAK', 'Buds opening'),
        ('FLOWERING', 'Flowers appearing'),
        ('FRUIT_SET', 'Fruit forming'),
        ('HARVEST_START', 'First harvest'),
        ('LEAF_FALL', 'Leaves dropping'),
        ('PRUNING', 'Pruned'),
        ('PEST_SIGHTING', 'Pest spotted'),
        ('FERTILIZING', 'Fertilizer applied'),
    ])
    date = models.DateField()
    notes = models.TextField()
Enter fullscreen mode Exit fullscreen mode

Users log events as they happen. The system learns patterns and predicts next year's events.

This became the killer feature. All 6 of my paid users use permanent plant tracking. None of them care much about the vegetable planning.

Lesson learned: The feature you think is core might not be what users actually pay for.

The Plot Designer: When HTMX Isn't Enough

Most of Leaftide uses HTMX. Click a button, server returns HTML, swap it in. Simple.

But the plot designer needed real interactivity:

  • Drag beds around a canvas
  • Resize placement rectangles
  • Real-time spacing calculations
  • SVG manipulation

HTMX can't do this. I needed JavaScript.

I built a custom SVG state manager:

class PlotDesigner {
    constructor(canvas) {
        this.canvas = canvas;
        this.beds = [];
        this.placements = [];
        this.dragState = null;
    }

    handleDragStart(event, element) {
        this.dragState = {
            element: element,
            startX: event.clientX,
            startY: event.clientY,
            originalTransform: element.getAttribute('transform')
        };
    }

    handleDragMove(event) {
        if (!this.dragState) return;

        const dx = event.clientX - this.dragState.startX;
        const dy = event.clientY - this.dragState.startY;

        // Update SVG transform
        this.dragState.element.setAttribute(
            'transform', 
            `translate(${dx}, ${dy})`
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

The designer syncs with Django via periodic saves. User drags stuff around in JS, clicks save, JS sends JSON to Django endpoint.

Hybrid approach: HTMX for 90% of the app, vanilla JS for the 10% that needs it.

User Acquisition: Reddit Was Everything

I launched on Product Hunt. Got 50 upvotes. Zero conversions.

Then I posted on r/BackyardOrchard about the permanent plant tracking feature.

3 paid users in 24 hours.

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

All 6 of my paid users came from Reddit. Not from:

  • Product Hunt
  • Google Ads (tried it)
  • SEO (still building)
  • Twitter

Lesson: Find where your actual users hang out. For me, it's niche gardening subreddits, not generic startup communities.

Technical Challenges

Challenge 1: Climate Data is Messy

NOAA data comes in weird formats. CSV files with inconsistent column names. Missing data for some locations. Stations that moved.

I spent weeks cleaning and normalizing it. Built a pipeline to:

  1. Download raw NOAA data
  2. Interpolate missing values
  3. Calculate frost probabilities
  4. Cache results per location

Not glamorous, but essential.

Challenge 2: Multi-Tenancy Without Going Crazy

Every user has their own garden, plants, and data. I needed proper isolation.

Django doesn't have built-in multi-tenancy. I built a middleware that sets a thread-local user context:

class CurrentUserMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if [request.user.is](http://request.user.is)_authenticated:
            set_current_user(request.user)
        response = self.get_response(request)
        clear_current_user()
        return response
Enter fullscreen mode Exit fullscreen mode

Then every model query automatically filters by current user. No accidental data leaks.

Challenge 3: Performance With Complex Calculations

Calculating planting windows for 100+ varieties across 365 days = expensive.

I cache aggressively:

  • Frost dates per location (rarely change)
  • GDD accumulation curves (pre-calculate for common dates)
  • Planting windows per variety (invalidate when user changes location)

Result: Page loads in <200ms even with complex calculations.

Monetization: Freemium That Actually Works

Free tier:

  • 15 seasonal varieties
  • 10 permanent plants
  • 2 custom varieties
  • Basic plot designer

Pro tier (£5/month or £45/year):

  • Unlimited everything
  • Advanced plot designer features
  • Priority support

The key: Free tier is genuinely useful. You can plan a real garden. But serious gardeners hit limits fast.

Conversion rate: ~12% of signups try Pro (7-day trial). About 50% of those convert to paid.

Lessons Learned

1. Build for a Real Problem You Have

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?"

This gave me:

  • Authentic understanding of the problem
  • Built-in validation (if I need it, others do too)
  • Motivation to finish (I actually wanted to use it)

2. Reddit > Product Hunt (For Niche Products)

Product Hunt got me 500 visitors and 0 conversions. Reddit got me all 6 paid users.

Why? Product Hunt users are browsing for cool products. Reddit users are searching for solutions to specific problems.

Find where your users are already asking questions.

3. The Feature That Converts Isn't Always What You Think

I thought the climate-aware scheduling would be the killer feature. It's technically impressive and solves a real problem.

But permanent plants tracking is what converts. Why?

  • Scheduling is a one-time lookup ("when do I plant tomatoes?")
  • Permanent plants are ongoing tracking ("when do I prune my apple tree next year?")

People pay for tools they'll use repeatedly, not one-time lookups.

4. HTMX Is Great Until It Isn't

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.

The lesson: Use the right tool for each job. Don't force a paradigm where it doesn't fit.

5. Solo Dev Means Ruthless Prioritization

I have a backlog of 50+ feature ideas. I've shipped maybe 10.

The difference between a side project and a business is saying no to good ideas so you can focus on great ones.

Every hour spent on a feature is an hour not spent on marketing.

6. Freemium Works If the Free Tier Is Generous

My free tier gives you:

  • 15 seasonal varieties
  • 10 permanent plants
  • 2 custom varieties
  • Full plot designer
  • All scheduling features

That's enough to actually use the app. Users upgrade when they hit limits, not because the free tier is crippled.

12% conversion rate suggests this works.

What's Next

Leaftide is live at leaftide.com. It's a real product with real paying users, but it's still early.

Current status:

  • 6 paid users (all from Reddit)
  • £30/month MRR
  • Solo developer
  • Launched October 2025

What I'm working on:

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

The biggest challenge? Marketing. Building the product was the easy part. Finding users who need it is the hard part.

If you're building a niche product, my advice:

  1. Build something you actually need
  2. Find where your users already hang out (not Product Hunt)
  3. Focus on features that create ongoing value, not one-time lookups
  4. Ship fast, iterate based on real user feedback

Want to try it? Check out leaftide.com

Questions about the tech? Drop them in the comments. I'm happy to dive deeper into any part of the stack.

Building something similar? I'd love to hear about it. Climate-aware software is an underserved space.

Top comments (0)