Benito Pedro Xavier

Full stack, concept to cloud

Interfaces, APIs, and the infrastructure underneath them.

Loading experience000
All case studies
Case studyPlatform2026

Flora

An operations console for regenerative farming, built on a real satellite pipeline

In plain English

A farm dashboard where a scheduled worker — never the app itself — asks satellites for ten different plant-health readings a field, so a farmer opens a map and sees exactly which corner of which field is struggling, backed by real Sentinel-2 imagery instead of a demo.

My role

Solo full stack developer

Timeline

Ongoing

Team

Solo

Status

Case study

Next.js 16NestJSTypeScriptPostgreSQL + PostGISDrizzle ORMBullMQ + RedisMapbox GL JSSentinel Hub (CDSE)Open-Meteo

Design

Product tour

Flora home dashboard with Regeneration Score, planting productivity, and crops stocked
Flora Fields screen with field cards and a Mapbox satellite basemap
Flora Crop Stress screen reading Contrasted NDVI over a real field boundary
The same field switched to NDMI, a different colour ramp for moisture instead of vigour
The same field switched to RECI, a red-edge chlorophyll index
Flora Tasks board with a real drag-and-drop Kanban
Flora Weather screen with six instrument cards over Open-Meteo data

Section 01

The problem

A first version of this idea existed already, built on Google Earth Engine — but Earth Engine only ships Python and JavaScript bindings, and it's a research compute platform, not an imagery API, which made it slow and awkward to build a real product on top of. Beyond that starting point, the actual product problem: a smaller regenerative farm has no in-house GIS or remote-sensing expertise, but still needs to know which part of which field is stressed, this week, without walking every hectare.

Section 02

How I approached it

The whole system is rebuilt around one invariant: no request ever calls the satellite provider. A NestJS worker polls Copernicus's Data Space Ecosystem on a schedule, decodes the returned imagery, computes ten spectral indices from a single Process API call, and writes pre-rendered PNGs to object storage plus rows to Postgres. The API — a separate NestJS service — only ever reads Postgres and Redis. That split is enforced by an actual test, not a comment. Every tenant table sits behind PostGIS row-level security *and* a repository-level filter, checked by a dedicated cross-tenant suite that asserts a 404 (not a 403) on another org's data. Contracts between the three services (web, API, worker) are Zod schemas in one shared package, imported by both TypeScript sides — never a hand-mirrored type.

Outcome

A working spine — register a field, watch a real stress zone appear from real Sentinel-2 data, act on it as a task — plus a home dashboard, a ten-index spectral switcher, and a weather screen reading a real Open-Meteo forecast. Several of the sharpest bugs never showed up in a code review or a unit test: they only appeared once a manual refresh ran against a real Copernicus account and a real field boundary, and got fixed the same way — by looking at what actually rendered.

Section 03

Going deeper

A read path that never touches the satellite provider

This is the one architectural rule the whole system is shaped around: Sentinel Hub is called from exactly one place, a scheduled BullMQ job in the worker, never from a request. The API reads only Postgres and Redis. A raster becomes a PNG in object storage — Cloudflare R2 in production, MinIO locally — referenced by its object key, never a signed URL, because a persisted signed URL just expires quietly in the database later. The rule is enforced by a test that asserts it, not by convention.

Ten indices for the cost of two

The pipeline started with one scheduled index (NDVI) and grew to ten — NDRE, EVI, MSAVI, RECI, MCARI, a PRI proxy, NDMI, NDWI, and VSDI alongside it, plus a true-colour render — by asking Sentinel Hub for all of them in a single Process API call instead of one call per index. Verified live against a real account: a refresh costs 4.667 Processing Units against a 4.0 baseline, a 17% increase for eight extra outputs, because output count turns out to be free and only the input bands matter. Two indices needed an honest product call rather than a formula: PRI has no real substitute on Sentinel-2 without a 531nm band, so it ships labelled 'PRI (proxy)' everywhere, never bare 'PRI'; a requested 'plain, fixed-domain NDVI' needs a client-side colour-mapped raster the pipeline doesn't build yet, so it's shown disabled with a tooltip explaining why, instead of a legend that quietly lies about what painted the pixels.

Bugs a code review can't catch

The Sentinel Hub client sent no `Accept` header on its Process API request. Copernicus silently returned a bare single TIFF instead of the requested two-file TAR, and `res.formData()` threw a parser error that read exactly like a token-endpoint failure — it took testing against the live account, not the mocked fixtures, to find the real cause. Separately, two indices without a division in their formula (VSDI, MSAVI2) evaluated to a finite, in-range number at pixels *outside* the field boundary, because the masking logic checked for a `0/0 = NaN`, which only NDVI-shaped ratios produce — so VSDI painted a field's whole bounding-box rectangle instead of clipping to its real, non-rectangular boundary. Caught from a live screenshot, fixed by switching the mask to the imagery's own SCL 'no data' class instead of a formula side-effect.

Tenancy enforced twice, not once

Every domain table — fields, crops, observations, stress zones, tasks — carries an `organization_id` filtered at the repository layer, and again by Postgres row-level security on the same table. A dedicated integration suite authenticates as one organization and asserts a 404, never a 403, on every other organization's resource, against real RLS, not a mock. The RLS catalog test is a named allowlist of exactly three `SECURITY DEFINER` functions the schema is allowed to have — anything else added later fails the build until it's explicitly reviewed and added to the list.

Section 04

The build process

1

Foundations, schema, tenancy

A pnpm/Turborepo monorepo, a Drizzle + PostGIS schema for ten domain tables with composite foreign keys, and identity with RLS enforced twice before any product screen existed. The design system's five PRO-tier Figma blocks were rebuilt as composites from free AlignUI components rather than paying for a seat.

2

The spine: Fields → Crop Stress → Tasks

Field CRUD and boundary drawing on a Mapbox map, then the satellite write path (a CDSE HTTP client, a decode-to-PNG raster pipeline, a BullMQ per-field scheduler), then the screen that reads it — raster overlay, stress-zone layer, colour-ramp legend — and finally a Kanban task board with a real @dnd-kit drag, verified against React 19 Strict Mode before any board code was written.

3

Home dashboard and a sourced score

Daily rollups, a weather snapshot table, and a Regeneration Score built from a real, cited formula — AAFC's agri-environmental performance index over soil cover days, Shannon evenness, and stress-free area share — instead of an invented composite metric.

4

Ten spectral indices

Widened the pipeline from one scheduled index to ten in a single Process API call, added a per-index colour-ramp registry and a vegetation-floor correction for indices that aren't NDVI-shaped, and built the on-demand true-colour render — which hit and got fixed by the identical SCL-masking bug for the identical reason.

5

Weather

An Open-Meteo hourly extension and six instrument cards — wind, UV, rain chance, sunrise/sunset, pressure, wind direction — each hand-verified against exported reference SVGs and real seeded data, down to fixing a dark-mode-only invisible-text bug from a static-versus-semantic colour token mismatch.

Section 05

How the pieces fit together

Each layer has one job. Read it top to bottom — that's roughly the path a request takes through the system.

Web

Next.js 16 App Router

AlignUI shell, Mapbox map, shadcn/Recharts charts — reads only Postgres-backed API endpoints and Redis-cached data, never a satellite provider.

API

NestJS

A separate service from the web app so it can share domain logic with the worker through packages/ instead of Next.js route handlers.

Worker

NestJS standalone + BullMQ

The only service allowed to call Sentinel Hub or Open-Meteo — a scheduled per-field job, not a request handler.

Database

PostgreSQL 16 + PostGIS 3.4

Drizzle ORM with a custom geography type; every tenant table behind RLS and a repository filter, both.

Object storage

Cloudflare R2 (MinIO locally)

Pre-rendered raster PNGs stored by object key, never a persisted signed URL.

Contracts

Zod, one shared package

The only shape crossing an app boundary — imported by both NestJS and Next.js, no codegen, no hand-mirrored types.

Section 06

What it does

Ten-index spectral switcher

NDVI, NDRE, EVI, MSAVI, RECI, MCARI, a labelled PRI proxy, NDMI, NDWI, and VSDI (shown as SMI), each with its own colour ramp — and an honest disabled state, with a tooltip, for the indices this pipeline can't actually produce yet.

Real stress-zone detection

A raster overlay clipped to the field's true polygon boundary via its imagery's own SCL 'no data' class, grouped detections with priority sorting, and a popover with real mutations against the task board.

Kanban tasks board

A three-column board with a real @dnd-kit drag, a server-computed midpoint for reordering, and a Water Used tile fed by the same `tasks.water_volume_m3` column the board writes.

A sourced Regeneration Score

Soil Cover Days, Shannon evenness, and stress-free area share, combined by AAFC's own published agri-environmental performance index — not a composite metric invented for the dashboard.

Section 07

The hard parts

A missing Accept header that looked like an auth failure

No `Accept: application/tar` on the Process API request meant Copernicus silently returned a single bare TIFF instead of the requested two-file archive, and undici's own parser error on `res.formData()` read exactly like a token problem. Only testing against the real, live account — not the recorded fixtures — surfaced the actual cause.

A masking bug that only two of ten indices had

Eight of the ten spectral indices are NDVI-shaped ratios, where a masked pixel's own math already produces `0/0 = NaN` outside the field boundary. VSDI and MSAVI2 have no division in their formulas, so that same masked pixel silently evaluated to a real, in-range number instead — painting a rectangle where a field polygon should have been. The fix switched masking to the imagery's own SCL 'no data' class instead of relying on a formula side-effect.

A synthetic seed that never clipped to a real boundary

The satellite seed script filled its field's entire bounding-box rectangle with valid pixels, since a synthetic raster has no reason to know about the field's real, often non-rectangular boundary the way a genuine CDSE response is clipped server-side. Found by looking at a rendered field, not by inspecting the script.

Section 08

What I took away

  • An architectural invariant is only real once a test can fail it — 'no satellite calls on a request path' stayed true because a test asserts it, not because a comment says so.
  • A missing header can produce an error message that points at the wrong layer entirely; the fix was verifying against the real live account instead of trusting where the stack trace pointed.
  • A masking rule that works for nine formulas out of ten is still a bug, not a rounding error — the tenth one needs its own real signal (SCL), not an assumption borrowed from the other nine.

Satellite pipeline

10 indices, one Sentinel-2 call

Tenancy

PostGIS RLS + repo filter, enforced twice

Request path

Zero calls to the satellite provider

Scoring

AAFC-sourced Regeneration Score, not invented

Contact

Let's build something worth shipping

Available for freelance projects, full-time roles, and technical consulting. Tell me what you're building and I'll tell you honestly whether I'm the right person for it.

Based in Rio de Janeiro, Brazil · Working with teams in any timezone · Fluent in English and Portuguese