I've been watching Sierra Nevada snowpack numbers obsessively for years. Not in a professional capacity — just as someone who grew up in Groveland, California, where the water that falls as snow in the high country eventually becomes the water in your tap, your river, your summer. You pay attention to it the way you pay attention to the weather. It's personal.
So last year I decided to build something that let me watch it properly. A live dashboard. Stream gages, historical comparisons, a map. The whole thing. And I wanted to do it without spending a dime.
Spoiler: it worked. Here's how.
Why I Built It
There are plenty of ways to check streamflow data. The USGS has their own site. There are apps. But none of them gave me what I actually wanted — a single view of the gages I care about, with historical context baked in, automatically updated, and hosted somewhere I could bookmark and share.
I also wanted to build it. That's the honest answer. I'd been doing a lot of geospatial work in Python and R and wanted a project that pulled it all together — data pipeline, visualization, web deployment — without needing a server, a database, or a credit card.
GitHub Pages and GitHub Actions turned out to be the entire infrastructure stack. Free, reliable, and already where my code lives anyway.
The USGS API: Better Than You'd Expect
The USGS National Water Information System has a public API that's genuinely well-documented and completely free to use. No key required. You just hit an endpoint with a site number and get back JSON with real-time and historical discharge data.
For the Sierra Streamflow Monitor I picked 8 gages across key watersheds — the Kings, the San Joaquin, the Merced, the Tuolumne, the American. Each one has a unique USGS site ID, and pulling current conditions is as simple as:
https://waterservices.usgs.gov/nwis/iv/?sites=11264500¶meterCd=00060&format=json
That returns the current discharge in cubic feet per second. For historical data I used the daily values endpoint, which goes back decades on most gages. That's what powers the spaghetti charts — overlaying this year's flow against the last ten years to show whether we're running high, low, or right on average.
The data is clean, consistent, and updated every 15 minutes. For a free public API, it's remarkably solid.
The GitHub Actions Pipeline
This is where the magic happens, and honestly it's the part I'm most proud of.
Every week, a GitHub Action wakes up, runs a Python script, fetches the latest data from the USGS API for all 8 gages, generates updated HTML charts, and commits the results back to the repo. GitHub Pages picks up the new files and serves them automatically. The whole cycle takes about 90 seconds.
The workflow file is about 20 lines of YAML:
on:
schedule:
- cron: '0 8 * * 1' # every Monday at 8am UTC
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install requests pandas matplotlib
- run: python fetch_streamflow.py
- run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
git commit -m "chore: update streamflow data" || echo "No changes"
git push
That's it. No server. No database. No monthly bill. The compute runs on GitHub's infrastructure, the data lives in the repo as static files, and GitHub Pages serves it for free.
The Leaflet Map
For the map I used Leaflet.js — a lightweight open-source mapping library that works great for static sites. Each gage gets a marker colored by its current flow relative to historical average: blue for above average, green for normal, orange for below, red for critically low.
Clicking a marker opens a popup with the current reading and a sparkline showing the last 7 days. The whole map loads from a GeoJSON file that the Python script generates along with the charts.
No Mapbox token. No Google Maps API key. Just Leaflet, OpenStreetMap tiles, and a static file.
Lessons Learned
A few things I'd do differently if I built it today:
Use pixi instead of bare pip. Dependency management on GitHub Actions gets messy fast. I've since switched all my projects to pixi for environment management and it's dramatically cleaner. I wrote about that switch here.
Cache the historical data. Right now the script re-fetches years of historical data every week, which is slow and wasteful. I should pull it once, store it in the repo, and only fetch the delta. It works fine, but it's inelegant.
Add alerts. The whole point of watching snowmelt is knowing when something unusual happens. I have a weather bot that already posts to Bluesky — wiring up a flow alert to the same pipeline would take an afternoon. It's on the list.
Start simpler. I built the spaghetti charts first because they were the thing I most wanted to see. The map came later. If I'd tried to build everything at once I probably would have given up. Pick the one thing you actually care about and get that working first.
The dashboard is live at bdgroves.github.io/sierra-streamflow. The code is all on GitHub. If you're watching a different watershed and want to adapt it, it should be pretty straightforward to swap in your own gage IDs.
The snowpack this year is behind average. The flows will tell us soon enough what that means for summer.