A free, public-domain plant API and the website that browses it.
- API — https://api.rootfast.org · interactive docs at
/v1/docs, spec at/v1/openapi.json - Web — https://rootfast.org
- License — MIT (code) + CC0 1.0 (data). See LICENSE.
Rootfast answers a simple question — I live here, what should I plant? — and answers it with data, not opinion. We join several pieces of beautiful public infrastructure into one queryable surface so downstream tools don't have to.
| Source | What we use it for |
|---|---|
| USDA PLANTS Database | Taxonomy, county-level distribution, native/introduced status, cultivation characteristics |
| USDA-ARS Plant Hardiness Zone Map (2023) | ZIP → zone and county → zone lookups |
| GBIF Backbone Taxonomy | The cross-source join key (gbif_taxon_key on every plant) |
| iNaturalist | Hot-linked CC-licensed species photos |
Everything is public domain or CC0. No keys, no commercial gating, no terms beyond attribution.
| Metric | Value |
|---|---|
| Plant taxa | 27,678 |
| States & territories | 55 / 56 (Connecticut pending USDA republish after their 2022 county abolition) |
| Counties | 3,230 |
| Distribution records | 2,501,631 (plant × county × nativity) |
| Hardiness ZIPs | 40,502 |
| Counties with hardiness zone | 3,199 / 3,227 |
| GBIF match coverage | 100 % |
| iNaturalist photos | 26,125 (94 %) |
| Schema version | 006 |
rootfast/
├── api/ Cloudflare Worker + D1 schema + build scripts
│ ├── src/ TypeScript Worker (Hono-free, raw fetch handler)
│ │ ├── index.ts Routes, rate-limit middleware, JSON helpers
│ │ └── openapi.ts Hand-maintained OpenAPI 3.0 spec
│ ├── schema/ SQLite-dialect D1 migrations
│ │ ├── 001_initial.sql Core schema (plants, counties, distribution, …)
│ │ ├── 002_photos.sql iNaturalist photo columns
│ │ ├── 003_hardiness.sql Hardiness zones (zones / by_zip / by_county)
│ │ ├── 004_wetland_index.sql Wetland-indicator index
│ │ └── 005_soil.sql SSURGO soil profile per county
│ ├── scripts/ Build + push tooling
│ │ ├── build.py Merge source SQLite chunks → unified rootfast.db
│ │ ├── build_chunks.py Split data dump into 4 MB chunks (D1 push)
│ │ └── push_chunks.sh Idempotent retry-loop wrangler push
│ └── wrangler.toml Worker config + D1 + rate-limit bindings
├── sources/ One folder per upstream
│ ├── usda-plants/etl/ Per-state distribution + profiles + tabs + GBIF + iNat
│ └── usda-hardiness/etl/ PHZM 2023: ZIP table + per-state polygons + county PIP
├── web/ Astro 4 SSR on Cloudflare Pages
│ └── src/pages/ Home, /plants, /counties, /states, /search, /docs, /about, /explore
├── docs/ Project-wide design notes
├── experimental/montana/ Sidelined MT-specific work (kept for reference)
├── LICENSE MIT (code) + CC0 (data)
├── README.md (you are here)
└── native-plant-finder-api.md Original project pitch
┌─────────────────────────┐
│ sources/usda-plants │──┐
│ (per-state fetch │ │
│ + GBIF + iNat enrich) │ │ source chunks
└─────────────────────────┘ │ (SQLite)
├─→ api/scripts/build.py ─→ rootfast.db ─→ D1
┌─────────────────────────┐ │ │
│ sources/usda-hardiness │──┘ ▼
│ (PHZM ZIP + county PIP) │ api/ (Cloudflare Worker)
└─────────────────────────┘ │
▼
web/ (Astro on Pages)
Each ETL pipeline emits a SQLite chunk matching the unified schema. The build
job merges chunks on gbif_taxon_key, runs FK + integrity checks, and emits a
data-only SQL dump split into ~4 MB chunks for D1's import path.
curl https://api.rootfast.org/v1/plants/PIPO # ponderosa pine, full detail
curl https://api.rootfast.org/v1/hardiness/zip/59715 # Bozeman, MT
curl 'https://api.rootfast.org/v1/search?q=ponderosa'cd api
npm install
npx wrangler dev --remote # serves on http://localhost:8787, queries production D1cd web
npm install
npm run devThe site reads from PUBLIC_API_BASE_URL (defaults to https://api.rootfast.org).
Point it at your local Worker if you're iterating on both.
cd api && npm test # Vitest — filter parser, paging, formatters, ETag
python3 -m pytest tests/ # ETL transforms — soil aggregation, name reducersCI (.github/workflows/ci.yml) runs the Worker typecheck + Vitest, the web
astro check, and the ETL pytest + a syntax-compile of every script on each
push and pull request.
# 1. USDA per-state fetch (parameterized; resumes from cache)
cd sources/usda-plants/etl
python3 scripts/fetch_usda_plants.py MT
python3 scripts/fetch_usda_plants_tabs.py # cultivation tabs
python3 scripts/match_gbif.py # GBIF backbone match
python3 scripts/fetch_inat_photos.py # iNat photo enrichment
# 2. USDA hardiness fetch (nationwide in one shot)
cd ../../usda-hardiness/etl
python3 scripts/fetch_hardiness.py
# 3. Emit per-source SQLite chunks
python3 sources/usda-plants/etl/scripts/build_sqlite.py
python3 sources/usda-hardiness/etl/scripts/build_sqlite.py
# 4. Merge → unified DB → chunked SQL → push to D1
python3 api/scripts/build.py
python3 api/scripts/build_chunks.py
api/scripts/push_chunks.shThe fetch scripts are idempotent — re-running picks up only what's missing.
Raw caches live under sources/*/etl/raw/ and are gitignored; small reference
files (states.json, state_polygons.json) are checked in.
Heavy consumers should download a snapshot rather than paginate the API. Each
release ships the full SQLite database (gzipped) and a per-table Parquet set,
plus a manifest.json with row counts and sha256 checksums.
Automated (recommended): push a data-v* tag. The
Dataset release workflow exports production
D1, rebuilds rootfast.db, packages the artifacts, and publishes a GitHub
Release. It needs a CLOUDFLARE_API_TOKEN repo secret with D1 read.
git tag data-v2026.07 && git push origin data-v2026.07Manual / local (you already have a built rootfast.db):
python api/scripts/export_release.py api/processed/rootfast.db api/processed/release --tag data-v2026.07
gh release create data-v2026.07 api/processed/release/* --title "Dataset data-v2026.07"Discovery paths and gotchas for each upstream are documented in
sources/usda-plants/docs/SOURCES.md.
Highlights:
- USDA PLANTS's distribution layer uses two distinct
plant_location_idspaces — you cannot join the ArcGIS layer to the PlantsService API by ID. We work around this with spatial polygon queries. - PHZM's "downloads" page is a misleading red herring (PNG/PDF only). The real data is in two ArcGIS FeatureServices the interactive map uses.
- Connecticut abolished its 8 counties in 2022 and replaced them with 9 planning regions. USDA has the new boundaries but no plant records linked to them yet — see SOURCES.md.
Issues, ideas, corrections, requests for specific regions — welcome. Rootfast is a quiet public-interest project. If you find a bug in the data, please open an issue with the USDA symbol or county FIPS so we can reproduce.
- USDA Natural Resources Conservation Service for PLANTS
- USDA Agricultural Research Service for the Plant Hardiness Zone Map
- GBIF for the Backbone Taxonomy
- iNaturalist photographers — each photo's individual CC license is preserved
- Cloudflare's free tier (Workers, D1, Pages) for hosting the whole thing