A phone with no signal cannot call for help. So someone else holds the timer.
Inspiration Every safety feature people assume they have — emergency SOS, Find My, a text to a friend — needs a network the traveler no longer has. The moment you actually need help on a remote drive is the exact moment your phone stops being able to ask for it.
The second problem is worse, and it's the one nobody designs for: silence is indistinguishable from being fine. Someone waiting to hear from you cannot tell "driving through a canyon with no bars" from "off the road in a ditch." By the time it's obvious something is wrong, the search area is the entire route.
So we inverted it. Instead of the phone calling for help after something happens, Blackout arms a watcher before the blackout — from the last moment the phone still has a network — and lets silence itself be the trigger.
What it does You plan a drive. Blackout forecasts every stretch where you'll lose signal, and roughly how long you'll be inside each one. You start the trip and send one link to one person.
Your phone sends a heartbeat every 30 seconds. On the last heartbeat before a forecast dead zone, it tells the server: I'm going dark, expect me back in 22 minutes. The watcher page — no app, no login — counts down. If you come back on time, it clears. If you don't, it goes amber, then red, on its own, with nobody touching it.
How we built it Next.js 15 (App Router) + TypeScript + Tailwind, Supabase (Postgres + Realtime), Mapbox GL JS for the map and Directions/Geocoding for routing, h3-js for cell indexing, @turf/turf for geometry. Deployed on Vercel.
Forecasting dead zones. Mapbox returns the route as a LineString. We walk it with turf.along every 1000 m, convert each sample to an H3 resolution-9 cell (≈ 0.105 km² each), dedupe, and resolve coverage per cell from FCC Broadband Data Collection data via BroadbandMap. Contiguous runs of weak cells become dark zones, with duration from the route's own average pace:
Runs shorter than two minutes are discarded as noise.
We round against the data on purpose. Carrier-modeled coverage is known to be optimistic, so the verdict rule is deliberately conservative:
marginal is treated as dark for alerting. We'd rather warn about a zone that turns out fine than stay quiet about one that doesn't.
Escalation is computed, not stored. This is the core design decision. The watcher derives its own state every second from the trip row plus the clock:
No cron job, no push service, no dependency on the phone that just went off the air. Supabase Realtime clears the alarm the instant a heartbeat lands, but the one-second interval is what makes the page go red when nothing arrives — the case that actually matters. Unplug the watcher's network and it still escalates.
The grace window is why the threshold is predicted reconnect + grace rather than a fixed timer: a 40-minute dark stretch must not alarm at minute 15.
Challenges we ran into The browser cannot read signal strength. There is no web API for dBm or bars, on any platform. Coverage had to be predicted from an external dataset, and connectivity detected only by whether heartbeats arrive. navigator.onLine is a liar — it returns true on Wi-Fi with no internet behind it — so real connectivity is a fetch to our own endpoint with a 5-second AbortController timeout.
The provider quota was 40× smaller than we planned for. We designed around ~1,000 calls/month. Querying the usage endpoint revealed the real limit: 250/month and 25 per day. A single 111-mile route needs 198 unique H3 cells — eight days of quota for one forecast. That forced three defenses: a coverage_cache table checked before every outbound call, a mock mode with hand-authored corridors on real highway alignments, and a resumable seed script gated on the actual remaining allowance.
A heartbeat was cancelling its own alarm. The client arms a dead zone ~600 m before entering it, and the server cleared expected_reconnect_at on any heartbeat while the trip was dark. The ordinary beats sent during that last stretch of coverage disarmed the trip — so the watcher fell back to counting plain silence and would have alarmed minutes early for a traveler exactly on schedule. The fix was reframing what a heartbeat means: it is proof of connectivity, so freshness wins while beats arrive, and the arm only governs once silence begins.
A trip that never got a GPS fix could never alarm. With no heartbeat there was no timestamp to count from, so the escalation branch was unreachable and the page parked on amber forever.
SUBSCRIBED doesn't mean streaming. Supabase Realtime reports a channel as subscribed before replication starts. A heartbeat landing in that gap was never delivered and never polled for, because polling is disabled once Realtime connects.
A service worker that could never update itself. Its cache version was a hardcoded string, so the cleanup step — which deletes caches whose name differs from the current one — never deleted anything.
What we learned Design for the absence of a signal, not the presence of one. Every reliable part of this system is triggered by something not happening. Degrade to "we don't know," never to a confident wrong answer. When quota runs out mid-plan, unknown cells resolve to covered — a phantom dead zone we can't stand behind is worse than a missed one. Query your limits, don't assume them. One call to a usage endpoint reshaped the architecture. Test what you'll actually demo. Storing a 35,000-point coast-to-coast route worked perfectly and made the watcher page an 834 KB, 7-second load. The database keeps every point; only the copy sent to the browser is thinned. What's next Twilio behind the existing notifyContact() seam, per-trip signed tokens so the watcher policy can be scoped to a single row, offline map tiles, and crowd-sourced ground truth to correct the modeled data.
Coverage data derived from the FCC Broadband Data Collection via BroadbandMap.com. Not endorsed by the FCC.
Built With
- broadbandmap-api
- fcc-broadband-data
- geolocation-api
- h3
- indexeddb
- mapbox
- mapbox-directions-api
- mapbox-gl-js
- next.js
- node.js
- postgresql
- pwa
- qrcode
- react
- screen-wake-lock-api
- service-workers
- supabase
- supabase-realtime
- tailwindcss
- turf.js
- typescript
- uber-h3
- vercel
- web-audio-api
- websockets





Log in or sign up for Devpost to join the conversation.