Inspiration

A volunteer stands at an urban stream with a field sampling sheet. They can observe insects under stones, spot birds on the bank, and notice unfamiliar plants — but interpreting those observations against the OneAquaHealth Health Assessment Framework is a specialist skill most citizens don't have. Most monitoring tools stop at "log the observation." StreamVitals closes the gap: it walks any volunteer through the official protocol indicators, explains what each observation means for human and animal health, and produces a structured, honest data record — without pretending to diagnose the stream.

What it does

StreamVitals guides a volunteer through five official OneAquaHealth indicators (Zenodo doi:10.5281/zenodo.20345207):

Three citizen-observable:

  • BMI-01 — Benthic Macroinvertebrates (state selection, photo, field notes, note quality gate)
  • BIR-04 — Birds (state selection, photo, field notes)
  • INV-11 — Invasive Alien Plants of the Riparian Corridor (state selection, photo, field notes)

Two lab-only (honest about what cannot be assessed in the field):

  • FCL-06 — Fecal Coliforms (auto-generated sample ID written on the physical container, real collection protocol, lab method citing ISO 9308-1 / Method 9222)
  • DIA-10 — Diatoms and Diatom Teratology (sample ID, scraping protocol, Naphrax preparation, EN 13942)

A deterministic assessment engine (streamvitals-assessment/1.0.0) maps each confirmed observation to a band — favorable, moderate, degraded, pending_lab, or unassessable — and attaches the evidence chain: the rule that fired, the factsheet passage behind it, and an explicit honesty caveat (e.g. "A BMWP score would quantify this — it requires laboratory identification"). The engine is a pure TypeScript module: no network, no clock, no model. Its own test suite reads the source file and fails the build if any of those patterns ever appear.

A bounded AI assistant answers factsheet-scoped questions. It refuses out-of-scope questions ("Is the water safe to drink?" → hard refusal with a pointer back to the assessment card). Groq llama-3.3-70b-versatile is used for the assistant only; it never touches the assessment decision.

How we built it

  • Next.js 16 App Router + TypeScript strict — single app/ directory, no dual-tree confusion
  • IndexedDB — all session data, photos, and GPS coordinates persist client-side
  • Assessment engine (src/lib/assessment/engine.ts) — pure rule tables, own-property lookups throughout (prototype-pollution fixed via fuzz testing), source-guard test
  • Note quality gate (src/lib/note-quality.ts) — warns on assessment language in free text, never blocks, writes note_flag to exports
  • Field Assistant (src/components/BoundedAssistant.tsx) — factsheet system prompt, out-of-scope detection, offline fallback with accurate per-indicator text
  • GBIF baseline (src/lib/gbif.ts) — browser fetches species occurrence counts directly from api.gbif.org when GPS is granted; disclosed at the point of asking
  • Share links — deflate-compressed URL fragments + QR (no account, photos excluded by design)
  • Service worker — network-first navigation, cache-first static assets, offline screen for uncached routes
  • CI/CD — GitHub Actions (lint → test → build → Playwright), Vercel auto-deploy on push

Challenges we ran into

  • Dual app directory bug — src/app/ and app/ coexisted silently; Next.js silently preferred root app/; months of edits to src/app/ were inert. Fixed by deleting src/app/ and verifying every page.
  • IndexedDB in headless browsers — required explicit Chrome flags (--enable-indexeddb) for Playwright e2e; WebKit (Safari/iOS) required storing data URLs instead of Blobs.
  • Hydration mismatches — navigator.onLine in useState initializer caused SSR/CSR divergence; fixed by initializing to true and correcting in useEffect.
  • Prototype pollution via crafted share links — assess('BMI-01', '__proto__') white-screened the review page; fixed with own-property lookups throughout the engine; caught by seeded fuzz testing.
  • GBIF consent gap — early label said "stays on your device" without disclosing the GBIF API call on review; corrected to name api.gbif.org explicitly before GPS is granted.

What we corrected before submission

We removed two claims that existed in an earlier build:

  1. An /api/analyze route — an AI-analysis endpoint that would have scored observations with a model, contradicting the shipped boundary.
  2. A provenance claim of FHIR R4 export — the export was never built; the endpoint now says so.

What we're proud of

  • 95 automated tests across 11 files — unit, integration, accessibility (zero axe violations at every impact level), offline, and e2e
  • A source-guard test that reads engine.ts at runtime and fails the build on any forbidden pattern
  • The lab-only flow — FCL-06 and DIA-10 tell volunteers what to collect and label, not what the result is
  • Every "Why this matters" passage is verbatim from the factsheet, with the section named

What we learned

Honesty is a feature. Every time a claim in the codebase outran what the code actually did — a false track statement, a fabricated API, an invented protocol detail — it damaged everything around it. The cleanest version of this submission is the one that says exactly what exists and nothing more.

What's next

  • Multi-volunteer shared sessions (server datastore with conflict-handling; share links are the current fallback)
  • Integration with the official OneAquaHealth Citizen Science App data pipeline
  • Field validation study with a OneAquaHealth research city pilot

Built With

Share this project:

Updates

Submission history