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, writesnote_flagto 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/andapp/coexisted silently; Next.js silently preferred rootapp/; months of edits tosrc/app/were inert. Fixed by deletingsrc/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.onLineinuseStateinitializer caused SSR/CSR divergence; fixed by initializing totrueand correcting inuseEffect. - 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:
- An
/api/analyzeroute — an AI-analysis endpoint that would have scored observations with a model, contradicting the shipped boundary. - 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.tsat 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
- gbif-api
- github
- groq
- indexeddb
- next.js
- playwright
- tailwind-css
- typescript
- vercel
- vitest
Log in or sign up for Devpost to join the conversation.