Last October, I stood in my apple orchard in Himachal Pradesh at six in the morning, holding a smartphone in one hand and a spray pump hose in the other. The weather app said clear skies. The wind felt wrong. The humidity felt heavier than the numbers suggested. I sprayed anyway, because the calendar said it was time, and by afternoon a surprise drizzle had washed most of it off the leaves.
I am a part-time apple grower and a full-stack developer. I do not have the luxury of guessing. When you lose a spray window to bad weather, you do not just lose chemicals. You lose protection during the exact weeks when scab and powdery mildew are waiting for the right conditions. So I am building the weather intelligence layer I wish I had that morning. This is the architecture, the code, and the plan.
Right now, Baagicha fetches weather from OpenWeatherMap and Open-Meteo, formats it for display, and discards it. There is no archive. There is no way to ask: "What did the forecast predict three days ago, and what actually happened?" Without that loop, the system cannot learn. It cannot tell me which provider is more reliable for my specific valley.
I am adding a weather_forecast_archive table that stores every fetch with metadata:
Schema::create('weather_forecast_archive', function (Blueprint $table) {
$table->id();
$table->foreignId('location_id')->constrained('user_orchards');
$table->date('forecast_for_date');
$table->dateTime('forecast_generated_at');
$table->enum('provider', ['openweathermap', 'openmeteo']);
$table->decimal('temp_max_c', 4, 1)->nullable();
$table->decimal('precipitation_mm', 5, 2)->nullable();
$table->decimal('wind_speed_kmh', 5, 2)->nullable();
$table->decimal('delta_t', 4, 2)->nullable();
$table->json('raw_response')->nullable();
$table->timestamps();
$table->index(['location_id', 'forecast_for_date']);
$table->index('forecast_generated_at');
});
A companion weather_actuals table will store observed weather after each date passes. The storage strategy is deliberately tiered: hourly granularity for the next forty-eight hours, because spray windows are measured in hours, then daily summaries for days three through sixteen. After thirty days, data aggregates into trend summaries. This keeps the database lean without sacrificing the detail that actually matters.
The goal is a feedback loop. If Open-Meteo consistently overestimates rainfall in my region by twenty percent, I want the engine to know that and weight it lower silently.
Baagicha already has pieces of this puzzle. A disease pressure estimator. A spray window calculator. A leaf wetness model. But they work in isolation. No single place understands the full picture of what is happening in my orchard on a given morning.
I am consolidating them into a unified WeatherEngine class that ingests current conditions and upcoming forecasts, then delegates to focused sub-services:
class WeatherEngine
{
public function __construct(
private ForecastAnalyzer $forecastAnalyzer,
private SprayRecommender $sprayRecommender,
private FertigationRecommender $fertigationRecommender,
private DiseaseRiskAssessor $diseaseRiskAssessor,
private DeltaTCalculator $deltaTCalculator,
) {}
public function generateDailyRecommendation(UserOrchard $orchard): WeatherRecommendation
{
$forecast = $this->forecastAnalyzer->analyze($orchard, days: 7);
$current = $this->getCurrentConditions($orchard);
return new WeatherRecommendation(
spray: $this->sprayRecommender->recommend($current, $forecast),
fertigation: $this->fertigationRecommender->recommend($current, $forecast),
diseaseRisk: $this->diseaseRiskAssessor->assess($current, $forecast),
deltaT: $this->deltaTCalculator->calculate($current),
alerts: $this->evaluateAlertRules($orchard, $current, $forecast),
);
}
}
The key design principle is that every output must include a reason a farmer can act on. The spray recommender, for example, returns one of four states — excellent, good, short, or avoid — plus an explanation:
class SprayRecommender
{
public function recommend(WeatherConditions $current, Forecast $forecast): SprayRecommendation
{
$reasons = [];
if ($current->windSpeedKmh > 15) {
$reasons[] = "Wind too strong ({$current->windSpeedKmh} km/h)";
}
if ($forecast->hasRainWithin(hours: 4)) {
$reasons[] = 'Rain expected in next 4 hours';
}
if ($current->deltaT > 10) {
$reasons[] = "Delta T too high ({$current->deltaT}) — droplets evaporate";
}
$recommendation = match (true) {
count($reasons) > 1 => SpraySuitability::AVOID,
$current->deltaT < 2 => SpraySuitability::CAUTION,
$current->deltaT >= 2 && $current->deltaT <= 8 => SpraySuitability::EXCELLENT,
default => SpraySuitability::SHORT,
};
return new SprayRecommendation(
recommendation: $recommendation,
reason: implode('. ', $reasons) ?: 'Conditions are favorable',
deltaT: $current->deltaT,
nextWindow: $this->findNextWindow($forecast),
);
}
}
Fertigation is completely new. Currently, Baagicha has no weather-aware fertilization advice at all. I am adding rules that check expected rainfall, soil temperature, and recent moisture. Light rain tonight means apply before three PM so the dissolve happens gradually. Heavy rain tomorrow means wait entirely. Soil below ten degrees Celsius means the roots will not absorb nutrients efficiently anyway. These are the rules my father taught me, translated into code.
Consumer weather apps optimize for picnics and commutes. Orchard weather optimizes for chemistry and biology.
Delta T is the difference between dry-bulb and wet-bulb temperature. It determines whether spray droplets reach the leaf or evaporate in mid-air. The ideal window is narrow: between 2 and 8. Below 2, droplets hang and drift into neighboring crops. Above 10, they evaporate entirely.
I am implementing the Stull (2011) wet-bulb approximation so the engine can calculate this automatically:
class DeltaTCalculator
{
public function calculate(float $tempC, float $humidityPercent): float
{
$wetBulb = $tempC * atan(0.151977 * sqrt($humidityPercent + 8.313659))
+ atan($tempC + $humidityPercent)
- atan($humidityPercent - 1.676331)
+ 0.00391838 * pow($humidityPercent, 1.5) * atan(0.023101 * $humidityPercent)
- 4.686035;
return round($tempC - $wetBulb, 2);
}
}
VPD, or Vapor Pressure Deficit, measures how aggressively the air pulls moisture from leaves. A high VPD means trees are stressed and need irrigation now, not when wilting becomes visible. These two numbers alone will change the quality of recommendations more than any UI redesign could.
The React Native app currently looks polished and lies beautifully. The forecast strip shows a perfect seven-day outlook. The spray status card glows green. All of it is hardcoded JSON left over from prototyping. I am replacing all of it with real data.
I am building a unified dashboard endpoint that returns everything the home screen needs in a single request:
{
"current": {
"temp_c": 22,
"humidity_percent": 65,
"wind_speed_kmh": 8,
"delta_t": 5.5,
"uv_index": 5.2
},
"today_recommendations": {
"spray": {
"recommendation": "avoid",
"reason": "Wind too strong (18 km/h). Rain expected at 4 PM.",
"next_window": {
"date": "2026-06-04",
"start_time": "06:00",
"end_time": "10:00"
}
},
"fertigation": {
"recommendation": "apply_now",
"reason": "Light rain (8mm) expected tonight. Apply before 3 PM.",
"best_time": "14:00"
},
"disease_risk": {
"score": 65,
"level": "moderate",
"active_diseases": ["apple_scab"]
}
},
"forecast_7d": [ /* ... */ ],
"active_alerts": [ /* ... */ ]
}
On the React Native side, I am building an offline-first hook that caches for twenty-four hours, because orchard connectivity in the Himalayas is unreliable:
export function useWeatherEngine(orchardId: string) {
return useQuery({
queryKey: ['weather', 'dashboard', orchardId],
queryFn: () => weatherApi.getDashboard(orchardId),
staleTime: 1000 * 60 * 60 * 24, // 24h offline cache
placeholderData: (prev) => prev, // Keep old data while fetching
});
}
A farmer with no signal will still see yesterday's recommendation with a stale badge instead of a blank loading screen. The migration is happening screen by screen: weather dashboard first, then home screen cards, then the alert inbox I am building from scratch.
I am designing a two-tier alert system because nobody needs a push notification about a spray window at midnight.
Every message will be bilingual, Hindi and English, written for farmers rather than translated by an algorithm. I am building the dispatch logic to respect quiet hours and batch intelligently:
class WeatherAlertOrchestrator
{
public function dispatch(UserOrchard $orchard, array $alerts): void
{
$critical = array_filter($alerts, fn ($a) => $a->severity === 'critical');
$warnings = array_filter($alerts, fn ($a) => $a->severity === 'warning');
foreach ($critical as $alert) {
$this->dispatcher->sendNow(
user: $orchard->user,
title: $alert->getTitle('hi'),
body: $alert->getBody('en'),
data: ['type' => $alert->type, 'action_url' => $alert->actionUrl]
);
}
if (count($warnings) > 0) {
$this->dailyDigest->queue($orchard->user, $warnings);
}
}
}
Mock data is dangerous. It lets you demo beautifully and forget that production is broken. I kept mock APIs alive for UI development, but I should have cut them off sooner. The longer fake data stays, the more it erodes trust — including your own trust in the product.
Builders who are also users have an unfair advantage. I do not need user research to know that wind speed matters more than cloud cover. I feel it every time I spray my own trees. That lived experience shortcuts a hundred product debates.
Offline-first is not a feature. It is a baseline. In the hills, signal comes and goes with the terrain. Caching the last recommendation and displaying it with a stale badge builds more trust than any real-time loading animation.
💡 Pro Tip: If you are building anything that depends on weather, start archiving forecasts on day one. The ability to compare prediction against reality becomes your most valuable asset six months later.
The Weather Engine is in active development. The database schema is drafted and partially migrated. The WeatherForecastArchiveService is implemented. The WeatherEngine class structure is in place, and the SprayRecommender and DeltaTCalculator are working in local tests. The React Native app still shows mock data on secondary screens, but the main dashboard is wired to the new API. The alert system is designed but not yet deployed.
My orchard is my testing ground. When the engine tells me Saturday morning is an excellent spray window, I will spray my own trees and see if it was right. When it warns me about scab risk, I will walk my blocks and check the leaves. The feedback loop is personal.
If you are building something similar, or if you are a grower who has strong opinions about what weather tools get wrong, I would love to talk. I am especially interested in hearing from other part-time growers who code, because the intersection of dirt and data is where the best agricultural software comes from.
Currently: Senior Engineer @ PrimeGurukul · Learning React Native · Open to freelance projects
Get in Touch
I'm available for freelance projects, consulting, and full-time roles.