Public satellite data is free and enormous, which is why most people look at it once and never load it again. TerraSentinel is a full pipeline over that data: ingestion, a versioned lake, SQL transforms, unsupervised ML, and a dashboard, running on free tiers from end to end.
Repo: swadhinbiswas/TerraSentinel · Dashboard: terrasentinel-dashboard.pages.dev
It watches four things: wildfire, deforestation, glacier and ice melt, and (as a fourth source) European power load. On the current run the dashboard reports 268,543 fire detections and 32 flagged days.
Who does the work
GitHub Actions runs the compute. Hugging Face Hub stores the bronze lake and the model registry. Turso serves the gold tables. Cloudflare Pages serves the dashboard. Nothing on the request path runs Python.
The transform layer is dbt on DuckDB, reading Parquet straight over HTTPS from the Hub. Bronze to staging dedupes detections and unifies MODIS and VIIRS brightness. Intermediate models build a dense region-by-day spine so zero days actually exist, a sparse H3 cell count for the map, and a seasonal baseline using a median and MAD over a circular window of fifteen days. The gold layer has one mart per source arm, and that split is deliberate.
A single combined "all ice" mart required every input to exist, so a missing Sentinel source also removed sea-ice results that were healthy and unrelated. Splitting the marts means an outage can only remove its own table, and the dashboard just reads whichever tables are present.
Lineage that means something
The model registry cannot share the dataset repo, because repo type is part of the address on the Hub and model cards are a model-repo feature. That is why there are two repos: one dataset, one model.
Keeping the lake in a git-backed dataset repo is what gives model lineage a real answer. MLflow logs the exact dataset commit hash behind every training run, and the published model is an IsolationForest on 27 strictly causal features traced to bronze commit 445d10f3. A bucket would not have given me that.
The model card measures instead of asserting. Model flags are compared against an independent median/MAD rule in the gold table, and precision, reference recall and Jaccard are printed. When the two agree almost completely, the card says so and gives the reason: the top 2.5 percent of fire days run about twenty times the rest, so the anomaly is easy to separate and every method finds the same days. That is the honest result, and it is more useful than a headline number.
Serving without a backend
Batch scoring applies the registered model to the latest gold features and writes anomaly_score into Turso as part of the scheduled job. The dashboard's API routes only do a fast SQL read, which keeps the edge functions inside Cloudflare's CPU budget and the pages quick. On the current run 76,607 rows are synced into Turso, including 1,462 model predictions, and the dashboard reads it in under ten milliseconds through Pages Functions.
One detail worth stealing: DuckDB's hf:// filesystem goes out anonymously unless a token is registered, and anonymous reads share the Hub's public rate-limit pool, so a scheduled job eventually returns HTTP 429. The token is registered deliberately outside dbt so it never reaches compiled SQL or an uploaded artifact.
What the data says
The numbers are real and checkable: 592 unit tests, a dbt build of 117 nodes, and a type-checked dashboard build, all runnable offline against a synthetic lake with deliberately injected anomalies. Arctic sea-ice extent in June 2026 is running about 1.3 million square kilometres below the 1981 to 2010 norm, a z-score near minus 3.3.
The project also ships a Databricks path (Asset Bundle, Unity Catalog, Workflows, MLflow) as a tested reference. The free path stays primary, and the point of the exercise is that the same lake and the same SQL move between them without a rewrite.
Reach me at swadhinbiswas.cse@gmail.com or on GitHub and LinkedIn.
No comments yet.