Live chain controls, closures, pass weather, and resort snow for the Tahoe passes.
Install · Screenshots · Features · Architecture · Quickstart · API · Tests · Data sources
![]() Home, light theme |
![]() Home, dark theme |
![]() Map, colored by control level |
![]() Trip brief |
![]() Guide |
| Feature | Details |
|---|---|
| Route status | Each route's chain control level when the app opens: R0 to R3 or Closed, and what it requires for the vehicle you specify |
| Map | Every Sierra crossing drawn on its highway geometry and colored by its current control level, with 80 Caltrans cameras, closures, CHP incidents, resorts, and passes. Tapping an item anywhere in the app opens it here |
| Push alerts | Watch a route and get a notification when its tier changes, a closure goes up, or a storm warning lands on its pass. Alerts cover only those three events |
| Pass forecasts | NWS pass forecasts and hourly snow accumulation, split into before departure and during the drive |
| Resort snow | 24 hour totals, base depth, and lifts open for 11 Tahoe resorts; off-season is labeled as off-season rather than reported as zero |
| AI trip brief | A language model writes the summary but can only restate verified facts. Each brief is validated for every active control and closure; if it fails validation, a deterministic plain-text brief is used instead |
| Offline and glove-friendly | Cached data with "as of" timestamps, large touch targets, and a short interactive guide for first-timers |
Important
The chain rules are code, not prose. R1, R2, and R3 vehicle requirements are encoded as structured logic from the published Caltrans definitions and covered by exhaustive unit tests. The AI presents them; it cannot alter them. And ChainCheck never tells you conditions are safe. That call is always yours.
flowchart LR
subgraph sources["Public data sources"]
CT["Caltrans district feeds"]
CHP["CHP dispatch XML"]
WX["NWS + Open-Meteo"]
RES["11 resort adapters"]
end
subgraph backend["Backend · FastAPI on Cloud Run"]
FEED["ca_roads feed layer"]
SNAP["Sierra snapshot"]
DIFF["Differ: tier changes,<br/>closures, storm warnings"]
API["JSON API"]
FCM["FCM push"]
end
subgraph clients["Clients"]
APP["Compose Multiplatform app<br/>Android · iOS in progress"]
end
CT --> FEED
CHP --> FEED
FEED --> SNAP
WX --> SNAP
RES --> API
SNAP --> API
SNAP --> DIFF
SCHED["Cloud Scheduler<br/>every 2 min in storms"] --> DIFF
DIFF --> FCM
API --> APP
FCM --> APP
Backend · Python 3.12, FastAPI, Cloud Run
- Road data comes from the ca_roads feed layer, which parses the public Caltrans and CHP feeds with per-district caching, conditional GETs, and stale-serve so a flaky feed degrades instead of disappearing.
- Forecasts come from api.weather.gov and Open-Meteo. Both free, no keys.
- Resort conditions come from public JSON feeds where resorts have them and light
per-resort scrapers where they do not. Each adapter fails alone, can be disabled
by config, and reports
nullfor anything unpublished rather than guessing. - Polling is adaptive: every 2 minutes during active weather, every 15 otherwise. A pure differ turns snapshots into events (tier changes, closures, storm warnings) that fan out to watching devices over FCM. Subscriptions are anonymous device tokens in Firestore.
- Route lines are extracted once from OpenStreetMap way data and baked into the package, so the map draws real pavement with zero runtime routing cost.
App · Kotlin, Compose Multiplatform
- One shared codebase for UI, data, and logic. Android ships first; the shared code already declares iOS targets.
- Two themes: "Sierra Light" for light mode and "Summit" for dark mode, with a high-contrast tier ring. The system setting chooses between them.
- Google Maps with a custom map style, tier-colored route polylines, and a small set of custom markers (ringed dots, tier pills, resort chips) rather than generic pins.
- The freshness line shows a live pulse and relative age ("Updated 2 min ago"), and switches to an amber "cached" state in dead zones.
AI safety design · how the brief stays grounded in facts
The trip brief pipeline is fact-assembly first, narration second:
- A pure, unit-tested function assembles everything the model may say: tier, every active control point, closures, alerts, forecast windows, snow split into before-departure and during-drive, and the vehicle ruling from the rules engine.
- Claude narrates those facts only, with one retry on validation failure.
- A validator checks the output: every active control and alert named, the current tier stated, no other tier mentioned, no safety promises. Anything that fails ships as the deterministic plain rendering instead.
- A 25-scenario eval set runs offline in CI and against the live model on demand.
Note
Google Play and the Apple App Store are coming in August 2026. Until then, Android users can install it directly in about a minute.
Android, right now:
- On your Android phone, open the latest release.
- Under Assets, tap the
.apkfile to download it, then open it. - Android will ask you to allow installs from your browser the first time (normal for apps not yet on the Play Store). Allow it, then tap Install.
- Open ChainCheck and pick your route. No account, no sign-up.
Full step-by-step, iPhone status, updating, and troubleshooting: Install guide.
By installing you agree to the terms of use. The privacy policy is short: no accounts, no tracking, location never leaves your phone.
Backend (Python 3.12+, uv recommended):
cd backend
uv venv && uv pip install -e ".[dev]"
uv run uvicorn chaincheck.api.app:app --reloadTip
The API is keyless by default. All core feeds are free and public. Set
ANTHROPIC_API_KEY if you want AI-narrated trip briefs; plain-text briefs work
without it.
Android (JDK 17+, Android SDK):
./gradlew :composeApp:installDebug -PchaincheckBaseUrl=http://10.0.2.2:8000Note
Maps need a Google Maps Android key in local.properties as MAPS_API_KEY=...
(restrict it to your package and signing certificate). Push needs a Firebase
project and its google-services.json in composeApp/. Both files are
gitignored; the app builds and runs without them, minus those two features.
The backend is client-agnostic JSON. The highlights:
| Endpoint | What it returns |
|---|---|
GET /v1/summary |
Every corridor's tier plus pass forecasts, in one call |
GET /v1/routes/{id} |
Control points, closures, and incidents for one corridor |
GET /v1/map |
Everything plottable: route geometry, controls, closures, incidents, webcams, passes, resorts |
GET /v1/resorts |
Snow totals, base, and lifts for 11 resorts |
POST /v1/rules/evaluate |
What a given vehicle must do at a given tier |
POST /v1/tripbrief |
The validated AI (or plain) trip brief |
PUT /v1/subscriptions |
Watch corridors with an FCM token |
Every data payload carries as_of and stale so clients can be honest about
freshness.
cd backend && uv run pytest # 150 tests: feeds, rules table, differ, briefs
uv run python -m evals.run_tripbrief # live-model eval set (needs ANTHROPIC_API_KEY)
./gradlew :composeApp:testDebugUnitTest # app unit testsCI runs the backend suite, lint, and an Android build on every pull request.
- Google Play and Apple App Store releases (August 2026)
- iOS build (shared code is ready, Xcode project in progress)
- Read-only web page for sharing route status with people without the app
- Chain controls, lane closures, cameras: Caltrans public district feeds
- Incidents: CHP dispatch feed
- Forecasts and winter alerts: National Weather Service
- Hourly snow: Open-Meteo
- Route geometry: OpenStreetMap contributors
- Resort conditions: each resort's public snow report, fetched gently and attributed in-app
Warning
ChainCheck is not affiliated with Caltrans, the CHP, the NWS, or any resort. Conditions change faster than any app. Verify before you drive: dial 511 or check quickmap.dot.ca.gov. Whether it is safe to drive is always your decision.
MIT. See LICENSE.




