Inspiration

Every disaster response plan quietly assumes a working network, and Indonesia's don't get to. When a quake hits Java or Jakarta floods, cell towers lose power or saturate within minutes — precisely the window when finding each other matters most. The people you are looking for are usually within a hundred metres. You simply can't see them. So we asked a narrower question than "how do we restore communication": can a phone tell the people nearby that it exists, using nothing but the radio it already has and no infrastructure at all? Bluetooth Low Energy advertisements turned out to be the answer — the same kind of broadcast a fitness tracker emits, which any phone can send and any phone can hear, with no pairing, no login, and no server that can be down. We named it Nyala, Indonesian for lit — alight, burning, switched on — because that is the entire idea: be a light that other people can find.

What it does

Nyala turns a phone into a short-range beacon. It broadcasts "I am here, at this position" over BLE every four seconds while simultaneously listening for everyone else doing the same, then draws them on a map that needs no tiles, no signal, and no server. Contacts appear as dots placed north-up relative to you with real distance in metres, and any one phone can flag itself as an assembly point — a fixed "come here, this is safe" beacon that everyone else's map homes in on. A phone out of direct range still reaches you, because nearby phones rebroadcast each other's frames up to three hops, de-duplicated by device and sequence number so nothing can loop back on itself.

The app speaks, vibrates, and draws all at once from first launch, and Settings can only turn a channel off, never on — in an emergency nobody is going to configure anything, so it has to arrive fully accessible. Alongside the live map, your GPS region is matched against Indonesian territories to surface the mitigation guidance that actually applies where you are: flood preparation in Jakarta, Drop/Cover/Hold on Java's fault lines, all bundled offline. And because a peer-to-peer app is invisible to anyone without two phones in a room, Nyala opens on a launch fork — Try the demo or Go live — where demo mode never touches the Bluetooth stack and runs the genuine interface off a scripted cast. Nothing Nyala knows ever leaves the handset, because there is nowhere for it to go.

How we built it

Nyala is React Native 0.86 with TypeScript, Zustand for state, NativeWind for styling, and react-native-svg for the map, plus a Kotlin native module we wrote ourselves for haptics. The wire format drove nearly every other decision: a single legacy BLE advertisement gives you roughly twenty usable manufacturer-data bytes, and that budget has to carry a device identity, a full coordinate pair, a timestamp, a sequence number, and an accuracy estimate. We packed it into nineteen bytes of raw integers — no text, no JSON — with one flags byte holding beacon type, relay status, protocol version, and remaining hops, three bytes of device ID, latitude and longitude as int32s scaled by 1e7, a uint32 timestamp, a uint16 sequence counter, and a single accuracy byte in metres where 255 means unknown. Even the service UUID was a budget decision: 0xFFF0 uses the 16-bit Bluetooth Base UUID pattern so Android's stack shortens it to two bytes, where a random 128-bit UUID would have left us six bytes total, not enough to say anything at all.

On top of that sits the decision we consider the core of the codebase — the honesty rule: a dot's position is a claim about where a person is, so it is never drawn from a guess. Every beacon passes two gates. First, do both phones have a real GPS fix, or is one still broadcasting the explicit (0,0) with accuracy 255 "no fix yet" placeholder? If either is, there is no position to plot, and that beacon goes to a direction unknown strip labelled only by coarse signal strength. Second, is the separation between them larger than their combined reported GPS error? If not, the bearing is dominated by noise, so the dot is still drawn — it is a real delta, just an untrustworthy one — but dashed, translucent, and prefixed with a tilde. At short range most beacons land in one of those two states, which is why the direction-unknown strip is a primary surface rather than a footnote.

The visual language treats Nyala as a field instrument rather than a consumer app, taking its cues from survey gear and marine chartplotters: hairline rules instead of borders, a manifest instead of cards, a status line instead of a title bar. Outfit carries every label and sentence while IBM Plex Mono is held back for figures only, because same-width digits mean a live-updating readout doesn't jitter as it counts. The palette is two hues on the blue–orange axis, which stays legible under every common form of colour vision deficiency, and ember is rationed to exactly one element — the arrow at the centre that is you — so the only warm thing on screen is the thing that's alive.

Challenges we ran into

The largest wall was iOS, which cannot broadcast at all — not "is throttled in the background," but cannot, in any state. CBPeripheralManager startAdvertising: honours only a local name and a list of service UUIDs and silently discards manufacturer data, which is where our entire payload lives. We spent real time confirming this wasn't a permissions mistake on our end before accepting it as a platform limit, and an iPhone can therefore hear Nyala beacons but never be one, making two iPhones mutually invisible. Rather than paper over it, we documented it plainly in the README.

Android handed us a subtler failure. Its OS scales vibration to zero amplitude whenever the ringer is in Silent or Do Not Disturb, and React Native's built-in Vibration fires under the default usage, so it vanishes along with everything else — we confirmed this live with dumpsys vibrator_manager, where even the system UI's own touch haptics report a scale of 0.00 in that state. Proximity haptics is a safety channel for blind users and should not disappear because somebody flipped a switch by accident, so we wrote a small native module that vibrates under USAGE_ALARM, one of the few usages Android refuses to silence for ringer state.

The mesh produced a bug that took a while to understand: a device that turned off its assembly-point flag would flip-flop on everyone else's map, because a relay was still faithfully rebroadcasting the older frame. The fix was a ten-second TTL on relayed frames combined with a rule that a frame older than the one you already hold is discarded rather than allowed to overwrite it. Two unmaintained native dependencies also refused to build — react-native-ble-advertiser pinned compileSdkVersion 28, and react-native-tts carried a buildscript block reaching for jcenter and Android Gradle Plugin 1.3.1 — both now fixed by committed patches that apply on install. Beyond those, we hit everything that is genuinely hard about sensor work: smoothing a compass across the 0°/360° wrap, adding hysteresis to RSSI buckets so a contact doesn't strobe between "near" and "medium," and making sure a spoken "getting warmer" is never computed across a switch from GPS metres to RSSI dBm, since those move on different scales in opposite directions and a beacon crosses between them the moment a fix arrives or drops.

Accomplishments that we're proud of

We built something that would rather admit uncertainty than lie to you. It would have been easier, and would have demoed better, to draw every contact as a confident dot on the map — instead the app tells you when it doesn't know, and that admission is a first-class surface in the interface rather than an error state hidden away. Sitting underneath it is a payload we are quietly proud of: identity, a full coordinate pair at roughly centimetre resolution, a timestamp, a sequence counter, and a self-reported error estimate, all in nineteen bytes.

We are also proud that accessibility here isn't a settings page. Voice, haptics, and the map all run simultaneously from first launch, and the voice deliberately never speaks a bearing, because "turn right" would imply more compass confidence than this system honestly has. The demo fork solved a problem we thought was unsolvable for a judged submission — a scripted cast that flows through the exact same state actions as a real broadcast, so every honest-placement branch, the voice guidance, and the mesh all have something live to react to on a single device, with nothing downstream able to tell the difference. And we wrote down what Nyala can't do: that nothing on air is encrypted or authenticated, that range is tens of metres, that the identifiers are Bluetooth SIG test values. A tool people might rely on in an emergency should not overstate itself.

What we learned

Constraints clarify design more than freedom does. Twenty bytes forced every single field to justify its existence, and the ones that survived are exactly the ones that matter. We also learned that the interesting part of a sensor app is the uncertainty rather than the sensor — reading GPS is easy, but deciding when a GPS-derived bearing has earned the right to be drawn on screen turned out to be the actual product, and most of our hardest thinking went there.

Building accessibility as a default rather than a setting produces a fundamentally different app. Once you can't rely on anyone completing setup, the defaults have to be correct out of the box and colour can never be the only signal, which is why assembly points also carry a glyph and approximate positions are also dashed and also prefixed. We learned that platform APIs have hard floors that no amount of engineering will move — no clever workaround makes an iPhone advertise manufacturer data — and that recognising a wall worth documenting instead of tunnelling through is its own skill. Finally, we learned to put tests where being wrong is expensive: the codec round-trip, relay dedup and hop logic, RSSI smoothing and bucket hysteresis, the placement decision, compass wrap, viewport projection, and territory matching, rather than on rendering.

What's next for Nyala

The biggest real-world gap is iOS, and closing it means a genuinely different protocol rather than a patch: if manufacturer data is closed to us, the path forward is packing identity and coarse position into the 128 bits of the service UUID itself. Mutual iPhone invisibility is the single limitation most likely to matter in an actual emergency, so it leads the list. Close behind is authentication — nothing today prevents a spoofed assembly point, and a false "come here, this is safe" is the worst failure this app could possibly have, so we want lightweight signatures that fit inside the byte budget, or an out-of-band pre-shared key for organised responders. Alongside that, a production deployment needs a registered Bluetooth SIG company ID and service UUID in place of the test values we're borrowing.

On the platform side, we want a proper foreground service so a phone in a pocket keeps broadcasting through an entire evacuation rather than only while the screen is on. Further out, a responder mode would give search-and-rescue teams a privileged beacon class that aggregates the mesh into a triage list — who has been seen, when, how far away, and who has gone quiet. And because the territory-matched mitigation guidance is a data file rather than code, taking Nyala beyond Indonesia is deliberately cheap: typhoon guidance for the Philippines or wildfire guidance for the western United States is a single JSON contribution away.

Built With

Share this project:

Updates