Lucas Rezende
Back to all work

Case 02 · AgroTech · 2026

AgroTrack

Livestock-management SaaS — MVP with a pilot running. Farm ERP, digital corral, NDVI via Sentinel-2 and an AI consultant with tool calling integrated with Postgres.

● case study
AgroTrack
ha in pilot
1,843
head in pilot
2,108
pastures w/ NDVI
27

Context

Where
the problem lived.

01 · Context

The setup

Brazil's livestock management market is dominated by ERPs with UX from the 2000s — heavy desktop apps, no AI, no satellite vision. Independent livestock consultants charge R$ 3-8k/month (USD ~600-1.5k) and still operate reactively — they show up after the cattle already lost weight.

02 · Problem

The real pain

Ranchers decide pasture rotation, supplementation and AI breeding with days or weeks of delay. Late decisions in livestock are absolute loss — kilograms of meat that didn't gain. Anyone running 2,000+ head loses thousands of dollars a month on decisions that could have been automated.

03 · Response

How I responded

Multi-tenant SaaS combining farm ERP, voice + RFID corral, NDVI from Sentinel-2 satellite imagery and an AI consultant doing tool calling over real farm data. Positioning: the Linear of livestock — a space where no national player ever shipped contextual AI integrated with satellite vision.

Stack

Every piece
with a reason.

No fashion picks. Each dependency here passed through a filter of maintenance cost, time-to-market and fit with a small team.

  • 01Next.js 16 (App Router)

    RSC for chart-heavy dashboards, Server Actions for mutations, Edge Runtime for AI routes

  • 02React 19

    useOptimistic so the corral works offline-first without UI flicker

  • 03Supabase (Postgres + Auth + Realtime + Storage)

    Multi-tenancy via RLS, realtime dashboards, storage for handling photos. 30+ tables prefixed agrotrack_

  • 04AI SDK

    Tool calling for the consultant, ephemeral prompt caching in 2 layers, per-feature model routing

  • 05Sentinel-2 (ESA)

    10m/pixel NDVI updated every 5 days, public free data — zero recurring acquisition cost

  • 06Leaflet + Turf.js

    GeoJSON pasture polygons, NDVI raster overlay, client-side area and centroid math

  • 07Recharts

    Weight evolution, ADG by lot, cash flow, stocking rate — all composable and typed

  • 08Capacitor

    PWA → native iOS/Android with 95% code reuse. Small team can't maintain 3 stacks

Architecture

How it all
talks.

Edge Functions handle heavy cron jobs (daily insights, NDVI refresh) and the AI chat route. Postgres is source of truth with farm_id RLS. Sentinel-2 enters through an Edge Function that normalizes tiles and stores aggregated results per polygon.

01 · client

PWA Next.js

iOS / Android via Capacitor

client

02 · edge

Edge Functions

AI chat · Cron · NDVI sync

edge

App Router RSC

Dashboard · Corral · Pastures

edge

03 · data

Postgres + RLS

30+ tables, multi-tenant

data

Storage

Photos · KMZ · Reports

data

Sentinel-2 ESA

NDVI 10m/5d

external

RFID wand

Bluetooth · Web BLE

external

LLM provider

Multi-tier routing

ai

Main flows

  • pwaRPCrsc
  • pwaStreamingedge
  • pwaBLErfid
  • rscRLS querydb
  • edgeservice roledb
  • edgetool callingai
  • edgetile fetchesa
  • edgeNDVI rasterstorage

Technical decisions

Seven choices
with explicit tradeoff.

Each one carries the problem, the options considered, the choice I made and the price I paid for it.

Decision 01

Per-feature model routing

Problem

General chat doesn't need the most expensive tier; NDVI analysis does. Using the most expensive model for everything explodes cost; using the cheapest sacrifices quality on critical decisions.

Options considered

  • aPremium tier everywhere — uniform quality, but ~3x more expensive than needed
  • bFast tier everywhere — cheap, but strategic insights come out generic
  • cFeature → tier router with workspace-level overrides

Choice

An agrotrack_ai_config table per workspace maps feature → tier. Fast tier for chat and voice, balanced tier for NDVI insights and data crosses, premium tier for yearly strategy. Enterprise customers can promote all routes to the balanced tier via override. Provider-agnostic — swapping providers means updating the mapping only.

Tradeoff accepted

More config complexity and an extra lookup per call. In exchange: ~70% token cost reduction vs default top-tier, without sacrificing quality on important decisions.

Decision 02

Two-layer prompt caching

Problem

Farm context (herd, pastures, current alerts) rarely changes between messages. Re-sending it on every call blows up input cost and latency.

Options considered

  • aRAG retrieval per query — embedding and vector search overhead
  • bInline context on every request — input cost linear with history length
  • cEphemeral prompt cache in two system blocks

Choice

System split into BASE_SYSTEM_PROMPT (static, cache 1) + farmContext (semi-static, cache 2). Both with cache_control: { type: 'ephemeral' }. Re-warm happens transparently when context changes.

Tradeoff accepted

Cache expires in 5 minutes — sparse conversations pay the re-warm. In exchange: ~85% cache hit rate after stabilization and ~80% input cost reduction vs inline system.

Decision 03

Sentinel-2 instead of IoT or owned drones

Problem

Mid-size ranchers (1,000-3,000 ha) don't have budget for distributed IoT. Owned drones add equipment cost, maintenance and aviation regulation. Without pasture vision, the AI is blind.

Options considered

  • aOwned drones — high resolution, but complex and costly operation
  • bField IoT network — continuous data, but infeasible CAPEX for mid-size customers
  • cESA Sentinel-2 satellite — public, free, 10m/pixel, 5-day revisit

Choice

Sentinel-2 through an Edge Function that downloads tiles, crops them by the pasture's GeoJSON polygon, computes mean NDVI + 90-day trend, stores the aggregate. Raw tiles aren't kept — only the per-pasture aggregate.

Tradeoff accepted

Medium resolution can't see fences or troughs. In exchange: zero hardware, zero maintenance, zero recurring cost. The customer perceives value from day one.

Decision 04

Multi-tenancy via Postgres RLS

Problem

Multi-farm SaaS with strict data isolation between customers. Schema decision affects complexity across the entire codebase — choice has to be deliberate.

Options considered

  • aDatabase per tenant — absolute isolation, operationally costly past 50 customers
  • bSchema per tenant — middle ground, migrations multiply
  • cRow-level security with policies — 1 schema, implicit filter on every query

Choice

All tables have farm_id, RLS policies read from agrotrack_farm_members by auth.uid(). Three roles: owner, consultant (accesses multiple farms), employee (only their farm). Service role bypass only for internal Edge Functions.

Tradeoff accepted

Every app query carries an extra implicit filter. In exchange: 1 schema, 1 database, 1 deploy. Operationally irrelevant up to 10k tenants — beyond that there's room to partition by region.

Decision 05

Tool calling instead of RAG in the AI consultant

Problem

The AI consultant has to answer about *this* farm, with fresh numbers. RAG over a daily dump hallucinates values. An answer like 'your pregnancy rate is at 45%' must come from the database, not from the LLM's imagination.

Options considered

  • aRAG over product docs — fine for operational questions, bad for live data
  • bRAG over daily tenant dump — hallucinates stale numbers, loses freshness
  • cTool calling with typed functions that query Postgres in real time

Choice

Tools exposed to the agent: get_animal(tag), get_lot_performance(lot_id, period), get_pasture_ndvi(name, days), get_financial_summary(period), query_pasture_capacity(), schedule_activity(...). Each tool runs a parametrized query with RLS applied.

Tradeoff accepted

Every tool requires its own code and maintenance when the schema changes. In exchange: exact answers backed by fresh data, zero numeric hallucination, and the agent becomes an actual agent — not a decorated chatbot.

Decision 06

Proactive insights via Edge Function + pg_cron

Problem

Ranchers are reactive — they only discover a problem (low ADG, degraded NDVI, empty cows post-AI) when they open the app. Without proactivity, the AI's edge dissolves after one week of usage.

Options considered

  • aOwn Node worker — paying for a 24/7 server to run for 1 minute a day
  • bExternal service (Inngest, Trigger.dev) — extra dependency, monthly cost
  • cpg_cron + Supabase Edge Function — native, costs cents

Choice

pg_cron triggers a daily Edge Function at 5 AM. It crosses lot ADG × pasture NDVI × monthly cost × days post-AI and creates 'Attention' cards in the ai_insights table + Web Push notification.

Tradeoff accepted

Edge Functions cap execution at 50s — giant farms (>5k head) require pagination. In exchange: zero extra infrastructure, fires every time the cron runs, costs a few cents per month.

Decision 07

PWA → Native via Capacitor

Problem

The app has to run in the corral — bad signal, dirty hands, nervous cattle. It must install on the phone, work offline, sync when back at the office. A small team can't maintain iOS, Android and Web separately.

Options considered

  • aNative iOS + Android separately — peak performance, 3x maintenance cost
  • bReact Native — shared code, but web is left out
  • cNext.js PWA + Capacitor packaging — 1 codebase, 3 targets

Choice

PWA-first with Service Worker for offline and Background Sync for the operation queue. Capacitor packages the same build for App Store and Google Play, with native plugins only where strictly needed (camera, Bluetooth BLE for RFID).

Tradeoff accepted

Performance ~5-10% below pure native on highly complex transitions — irrelevant for the real use of this app. In exchange: 95% code reuse, 5x faster time-to-market, maintenance in a single tree.

Highlighted implementation

One slice
that tells the story.

lib/ai/chat-with-farm.ts
typescript
// AI Consultant: per-feature model routing + 2-layer prompt caching
async function chatWithFarm({
  farmId,
  message,
  feature,
}: ChatRequest) {
  const [config, farmContext] = await Promise.all([
    getFarmAIConfig(farmId),
    getFarmContext(farmId), // herd + pastures + alerts
  ]);

  const model =
    config.feature_models[feature] ?? DEFAULT_MODELS[feature];

  return anthropic.messages.create({
    model,
    max_tokens: 1024,
    system: [
      {
        type: "text",
        text: BASE_SYSTEM_PROMPT,
        cache_control: { type: "ephemeral" },
      },
      {
        type: "text",
        text: farmContext,
        cache_control: { type: "ephemeral" },
      },
    ],
    tools: FARM_TOOLS, // get_animal, get_lot_performance, get_pasture_ndvi…
    messages: [{ role: "user", content: message }],
  });
}
Feature → model router + system in 2 cached blocks. ~85% cache hit, ~$0.02 per conversation in production.

Technical metrics

Numbers
that matter.

  • Status

    MVP

    pilot running, pre-paying customers

  • Avg. cost per AI conversation

    ~$0.02

    measured in pilot · 9 initial conversations

  • Satellite resolution

    10m/pixel

    Sentinel-2 ESA, 5-day revisit

  • Postgres tables

    30+

    100% under multi-tenant RLS

  • Tools available to the agent

    6

    animal, lot, NDVI, finances, capacity, schedule

  • Multi-tenant

    RLS

    farm_id isolation in every query

Retrospective

What I'd do
differently today.

Decisions I'd reverse knowing what I know now. Not regret — the kind of learning that changes the next project.

  1. 01

    Lean schema before pretty schema

    I created tables (sync_queue, ai_insights, soil_analyses, pasture_managements) before having any code writing to them. Rich, empty schemas turn into cognitive noise. Now I only create a table when there's a seed migration + writer code ready.

  2. 02

    Per-feature model routing paid off from day one

    The initial instinct was to use the balanced tier for everything — uniform quality. Mapping feature → tier added some extra complexity, but saves ~70% in token cost as measured in the pilot. Explicit per-workspace configuration also opens the door for a future Enterprise customer who wants to override defaults.

  3. 03

    Tool calling > RAG when data is the product

    I considered RAG over a daily tenant dump. In practice, RAG hallucinates stale numbers — a rancher asks 'what's my pregnancy rate now?' and the LLM answers based on yesterday's snapshot. Typed tools querying Postgres in real time was more work but eliminated numeric hallucination.

Gallery

In use.

07 frames

AgroTrack — Case study · Lucas Rezende