A local-first emergency messaging prototype. Nearby Android phones carry signed SOS messages without a tower, server or account.
Translation pending
SocialJava · TypeScriptGitHub @Gautam-amanOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
An offline-first, self-healing emergency communication network using peer-to-peer mesh networking, multi-hop routing, and edge AI to deliver SOS messages without internet connectivity.
Taken from the repository README
Loading the full README.
Read the full README ↗Created and maintained by @Gautam-aman.
Open-source contributions are welcome! 🤝
Offline-first emergency messaging over a peer-to-peer device mesh.
You don’t need mesh-networking expertise to make a useful first contribution. Start with documentation, Java, Android, frontend, or reproducible testing.
Browse open Good First Issues · Contributor quickstart · Explore contributor opportunities
Check the issue’s current assignees, comment /assign, and wait for confirmation.
Then fork, build, run the relevant tests and open a focused PR. Ask for help in
the issue; use /unassign if you need to release it.
Real-device testing is especially valuable: start with the Android hardware guide, even with one phone. Use dummy messages and report the build, device, steps, actual results and any blocked checks. Simulation is not evidence of real multi-hop radio performance.
Want to verify the mesh routing without setting up Android phones or a gateway? You only need JDK 21 and Maven 3.9+. In a fresh clone, open a terminal at the repository root, check the versions, then run:
java -version
mvn -version
mvn -pl packages/mesh-simulator -am clean package
On Windows PowerShell, select the JAR that Maven just built:
$jar = (Get-ChildItem packages/mesh-simulator/target/*-runnable.jar).FullName
java -jar $jar --topology ring --nodes 10 --messages 15 --seed 50 --rounds 30 --out report.json
On macOS or Linux, a POSIX shell expands the JAR path directly:
java -jar packages/mesh-simulator/target/*-runnable.jar --topology ring --nodes 10 --messages 15 --seed 50 --rounds 30 --out report.json
The build should end with BUILD SUCCESS. The simulator prints a
Nodes: summary and Wrote .../report.json on success. The JSON report is
written to report.json in the repository root; it can be explored in the
Mesh Replay Lab. The delivery rate counts
directed messages only, because broadcasts cannot be acknowledged.
This uses the real routing engine with simulated radio, not physical handset or radio testing. The first Maven run may need to download dependencies, so the heading is not a guaranteed wall-clock time. For the full seven-stage demonstration and failure scenarios, follow the demo guide.
ResQNet is a mesh networking layer for phones. It carries emergency messages between nearby devices when the infrastructure they normally depend on is gone — a cell tower without power, a flooded exchange, a cut fibre.
The radios are already in the phones. So are the batteries, and so are the people who need to reach each other. ResQNet is the layer that makes those radios useful to one another.
A handset composes an SOS. The message is handed to whichever devices are connected, they hand it onward, and it keeps moving until it reaches the device that owns the responder — or until its lifetime expires. No server is involved at any point on that path. The coordination gateway is optional, runs only when there is connectivity, and is never on the emergency path.
This is experimental research software. It is not a substitute for official emergency services and must not be relied upon as one.
Watch the full 38-second explainer (MP4, 1.2 MB) — also a concept visualization, with the same disclaimer applying.
A step-by-step demonstration script with exact commands and failure injection is in docs/demo.md.
The public web app also includes a Mesh Replay Lab at /lab. It imports a
report written by the Java simulator, replays the recorded events, inspects the
handoffs behind a message and compares two runs. Reports are read locally and
every view is labelled SIMULATED — NOT REAL RADIO TELEMETRY. The usage
guide, report schema notes and known limits are in
docs/mesh-replay-lab/README.md.
Disasters do not fail gently. The first thing to go is usually the thing everything else depends on.
| What fails | What still works |
|---|---|
| Cell towers lose power | Bluetooth and Wi-Fi radios stay on |
| Backhaul is cut | Handsets are still in each other's range |
| The internet is unreachable | People are still holding the phones |
| An account system is unreachable | A key pair is already on the device |
Infrastructure-scale networks need infrastructure. A mesh does not. The constraint is honest and small: if a message cannot reach a recipient within the hop budget before its lifetime expires, it is reported as expired rather than as delivered.
SENT_TO_PEER and ACKNOWLEDGED are never
collapsed into one another.
sequenceDiagram
participant A as Phone A - Sender
participant B as Phone B - Relay
participant C as Phone C - Responder
Note over A: No internet. Compose SOS offline.
A->>B: Signed SOS envelope - hop 1
Note over B: Persist to SQLite and forward
B->>C: Signed SOS envelope - hop 2
Note over C: Verify origin signature
C-->>B: Application-level ACK
B-->>A: Forward ACK to sender
Note over A,C: Directed messages are encrypted with HPKE, while public SOS stays clear
Four rules govern forwarding:
UNVERIFIED, not trusted.SENT_TO_PEER and ACKNOWLEDGED are different states and are never collapsed.
A successful Nearby Connections payload transfer proves one radio hop succeeded.
It does not prove a person received anything. The UI renders SENT_TO_PEER as
"Handed to a nearby device", never as delivered, because a responder
deciding whether to hold a position needs to know the difference.
graph TB
subgraph Mesh["Device mesh — no infrastructure required"]
A["<b>Handset A</b><br/>UI · MeshService<br/>MeshRouter · SQLite queue"]
B["<b>Handset B</b><br/>UI · MeshService<br/>MeshRouter · SQLite queue"]
C["<b>Handset C</b><br/>UI · MeshService<br/>MeshRouter · SQLite queue"]
A <-->|"Bluetooth / Wi-Fi Direct<br/>Nearby Connections"| B
B <-->|"Bluetooth / Wi-Fi Direct"| C
end
subgraph Optional["Optional — only when connectivity exists"]
G["<b>Coordination gateway</b><br/>Spring Boot · PostgreSQL<br/>incidents · audit trail"]
W["<b>Command centre</b><br/>React · WebSocket"]
end
A -.->|"metadata only, never bodies"| G
C -.->|"metadata only"| G
G <--> W
style Mesh fill:#0f2a24,stroke:#1f7a6d,color:#e6f7f3
style Optional fill:#1a1a2e,stroke:#3d3d6b,color:#e0e0f0
The gateway and command centre sit outside the mesh on purpose. A handset may synchronise with the gateway when it has connectivity and a responder console reads from it, but nothing on the emergency path depends on it existing. Synchronisation sends delivery metadata — ids, states, timestamps, attempt counts. Message bodies are never sent to it.
The full design, including the reasoning behind each trade-off, is in docs/architecture/README.md.
apps/android — Java, Android SDK, Nearby Connections.
ConnectionsClient API, strategy
P2P_CLUSTER.apps/android/app/build.gradle rather than at release time.Minimum supported version is Android 8.0 (API 26). Debug builds carry a
.debug application-id suffix, so a debug and a release build install side by
side without replacing each other.
Background restrictions matter. Doze mode, battery optimisation and manufacturer-specific task killers can suspend the foreground service. See Known limitations.
apps/web — React 18, TypeScript, Vite, Zustand.
The client distinguishes the gateway is unreachable from there are no incidents. Conflating them would let a responder act on a dashboard that is quietly reporting an empty queue because it could not reach the server.
Generated simulator runs are labelled as simulated at every layer — the dashboard, the API, and a database column. There is no path by which generated data is presented as field telemetry.
| Layer | Technology |
|---|---|
| Mesh core | Java 21, zero runtime dependencies, runs unchanged on Android |
| Simulator | Java 21, discrete-event, deterministic from a seed |
| Gateway | Java 21, Spring Boot 3.5, PostgreSQL, Flyway, JPA/Hibernate |
| Android | Java, Android SDK, Nearby Connections P2P_CLUSTER, SQLite, Robolectric |
| Web | React 18, TypeScript, Vite, Zustand, WebSocket with polling fallback |
| CI | GitHub Actions: Java, backend (H2 + PostgreSQL), Android, instrumented Android, web |
Requires JDK 21, Maven 3.9+, Node.js 24 LTS for the web app.
git clone https://github.com/Gautam-aman/ResQNet.git
cd ResQNet
# Java modules: mesh core, simulator, gateway
mvn -q package
# Web
cd apps/web && npm ci && npm run build
java -jar services/backend/target/backend-*.jar \
--spring.profiles.active=dev
Requires RESQNET_JWT_SECRET (32+ characters) and, to let any device
synchronise, RESQNET_GATEWAY_TOKEN. Without them the application refuses to
start, which is deliberate.
The dev profile seeds two accounts with the password resqnet-dev:
responder and admin. They exist only under dev.
To use PostgreSQL rather than H2:
RESQNET_DB_URL=jdbc:postgresql://localhost:5432/resqnet \
RESQNET_DB_USER=resqnet RESQNET_DB_PASSWORD=resqnet \
java -jar services/backend/target/backend-*.jar \
--spring.profiles.active=dev
The backend jar has no bundled H2 driver, because H2 is test scope. Pointing it at H2 fails at startup by design; it targets PostgreSQL.
cd apps/web && npm run dev # http://localhost:5173
The command centre is at /command. The dev server proxies /api and /ws
to localhost:8080.
cd apps/android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
Release signing reads four properties from your own Gradle home and is
deliberately unset in the repository. With any of the four missing,
./gradlew assembleRelease produces an unsigned APK — it does not fall back
to the debug key, because a debug-signed APK is installable and looks exactly
like a release until somebody tries to ship it.
# ~/.gradle/gradle.properties — never the project file, never committed
RESQNET_KEYSTORE_FILE=/absolute/path/to/resqnet-release.jks
RESQNET_KEYSTORE_PASSWORD=...
RESQNET_KEY_ALIAS=resqnet
RESQNET_KEY_PASSWORD=...
The first publicly downloadable Android build ships with v0.2.0-beta, published as a pre-release.
| Package | dev.resqnet.android |
| Minimum Android | 8.0 (API 26) |
| Signing | Release keystore, verified with apksigner |
| Checksums | SHA256SUMS alongside every release asset |
Installation is covered in docs/release/0.2.0-beta/INSTALLATION.md. Verify the SHA-256 checksum before installing, and only install APKs obtained from this repository's releases page.
Installing APKs outside the Play Store should only ever be done from a source you trust. This build is experimental and is offered for evaluation.
packages/mesh-simulator is a discrete-event model of the same routing
code. It is not a mock: it runs the real MeshRouter with a modelled radio.
CP=packages/mesh-simulator/target/classes:packages/mesh-core/target/classes
java -cp $CP dev.resqnet.mesh.simulator.SimulatorCli \
--topology mesh --nodes 12 --messages 40 --seed 7 --rounds 60 \
--out /tmp/report.json
Useful flags: --drop 0.2 to lose 20% of frames, --churn 6 to cut and restore
a link every six rounds, --partitioned to start with the busiest relay offline.
RESQNET_GATEWAY_TOKEN=dev-device-token \
java -cp $CP dev.resqnet.mesh.simulator.SimulatorCli \
--topology mesh --nodes 14 --messages 50 --seed 11 --quiet \
--gateway http://localhost:8080/api/v1/simulations
Ingestion authenticates as a device, using the shared token. This is deliberate: a simulator is a generator, not a handset, and giving it a human session would imply an accountability model that does not exist.
java -cp $CP dev.resqnet.mesh.simulator.demo.DemoScenario
Seven stages over the real MeshRouter with a modelled radio: composition with
no network, two-hop delivery, a relay vanishing mid-flight, recovery, gateway
sync without content, end-to-end confirmation, and the responder view. It exits
non-zero if any stage misbehaves, and three tests assert on that result.
The same demonstration runs from a release artifact with no infrastructure:
java -cp dist/resqnet-simulator-v*.jar \
dev.resqnet.mesh.simulator.demo.DemoScenario
| Suite | Tests | Command |
|---|---|---|
| mesh-core | 154 | mvn -pl packages/mesh-core test |
| simulator | 51 | mvn -pl packages/mesh-simulator -am test |
| backend (H2) | 60 | mvn -pl services/backend test |
| backend (PostgreSQL) | 7 | runs in CI against postgres:16-alpine |
| Android unit + Robolectric | 56 | cd apps/android && ./gradlew testDebugUnitTest |
| Android instrumented | 33 | cd apps/android && ./gradlew connectedDebugAndroidTest |
| Web | 46 | cd apps/web && npm run test |
Seven simulator tests are tagged slow because they run whole simulated meshes
rather than methods. They are excluded from an ordinary build so a push stays
quick, and CI asks for them explicitly with -DexcludedTestGroups=, so nothing
is silently skipped.
Instrumented tests run in CI at 320×640 @ 160 dpi, and have also been run locally at 1080×2400 @ 420 dpi. Multi-emulator testing has not been performed.
The PostgreSQL integration tests are not run by default, because they need a live server:
mvn -pl services/backend -am test \
-Dspring.profiles.active=postgres \
-Dtest=PostgresMigrationIT \
-Dsurefire.failIfNoSpecifiedTests=false
with TEST_PG_URL, TEST_PG_USER and TEST_PG_PASSWORD pointing at a live
instance.
Delivery figures are computed over directed messages only. A broadcast has no destination, so nothing can acknowledge it, and counting broadcasts in the denominator capped the reported rate below 100% however well the mesh performed. Both counts are reported separately.
What limits delivery is the graph, not the router. TopologyDiagnostics
measures the shapes the simulator builds. MESH is a circulant band of diameter
n/4, so at 500 nodes only a quarter of node pairs are inside a 32-hop budget.
Across hop budgets at 100 nodes the router reaches 98–100% of what the graph
permits, and at 500 nodes the measured 21.6% sits directly under the 25.7%
structural ceiling. The full argument, including seven causes that were tested
and ruled out, is in docs/routing-analysis.md.
Mesh topology, one seed, same messages at every size:
| Nodes | 4 hops | 8 hops | 16 hops | 32 hops |
|---|---|---|---|---|
| 10 | 90.0% | 90.0% | 90.0% | 90.0% |
| 50 | 15.0% | 50.0% | 90.0% | 90.0% |
| 100 | 24.0% | 44.0% | 76.0% | 88.0% |
| 500 | 2.4% | 4.0% | 10.4% | 21.6% |
The headline finding is that hop budget, not mesh size, decides whether a message crosses. A flood travels one hop per relay, so a mesh wider than the hop budget cannot be crossed no matter how long the sender waits.
Full numbers, with the host they were measured on, are in docs/benchmarks/results.md.
These are simulation results, not field measurements.
This distinction is the most valuable thing in the repository. It is easy to erode, so it is stated explicitly:
| Evidence class | Status |
|---|---|
| Automated unit and JVM tests | ✅ Passing, run in CI |
| Robolectric tests | ✅ Passing |
| Emulator instrumentation | ✅ Passing, at two geometries |
| Simulator multi-hop demonstration | ✅ Passing, deterministic from a seed |
| Backend against real PostgreSQL | ✅ Passing in CI |
| Physical devices | ⚠️ Developer-reported only |
| Three-phone multi-hop | ❌ Not verified |
Physical-device testing. The developer reports the Android application
working on two physical phones. That is a user report, not independently
verified evidence, and it is not evidence of multi-hop routing: with two phones
in range of each other there is no third device to relay through, so
P2P_CLUSTER connects them directly and no hop is taken.
Not verified: end-to-end offline delivery between three or more physical devices, cryptographic acknowledgements over real radios, physical radio range, and battery behaviour under continuous mesh participation.
The procedure for running a proper hardware test is in apps/android/DEVICE_TESTING.md.
SimulationReport.volatileJsonFields() names exactly three things that cannot
be byte-stable across runs of the same seed, and everything else is
reproducible: the wall-clock and throughput measurements, and the freshly
generated device ids and key fingerprints.
Every routing decision, counter, latency and hop distribution is a function of the seed. This is enforced by a test that forks the CLI in a separate JVM, because the failure mode it guards against is invisible in-process: the JDK randomises its immutable collection iteration order once per JVM, so two runs in one process agree with each other and disagree with every other process.
python3 scripts/compare_reports.py /tmp/a.json /tmp/b.json
Threat model in docs/security/threat-model.md. E2EE overhead measurements are in docs/benchmarks/e2ee.md.
UNVERIFIED, not trusted. Keys are exchanged explicitly, out of band, and the
two people compare short fingerprints in person.SHA256withECDSA). A device's id is derived from its public key, so there is
no account to lose and no server that can impersonate a handset.SOS_PROTECTED /
TEXT_PROTECTED ciphertext and route on the signed header without decrypting.DEVICE, because no human triaged it.Known limitations are listed in the threat model rather than left implicit. The most important: the shared gateway token is a bearer secret, so it is revocable by rotation but not attributable, and every device holding it shares one identity.
Directed payloads are encrypted; public broadcasts are not. Envelope signatures guarantee authenticity and integrity. A message addressed to one verified peer is also confidential from relays and eavesdroppers. A public SOS broadcast is visible to anyone in range. Envelope metadata (origin, destination, type, timestamps and size) remains visible on both paths.
Stated plainly so they are not discovered by a reviewer.
Validation gaps
Platform behaviour
P2P_CLUSTER is an M-to-N strategy:
every device within radio range connects to every other, so with three phones
side by side, A to C is one hop and no relay is involved. A demonstration
has to put the phones out of range of each other for the relay to exist at all,
which is why DEVICE_TESTING.md insists on a closed door and on confirming C
does not appear in A's peer list before sending.P2P_CLUSTER authentication token is used. Google's API makes it
optional. Trust is enforced one layer up, by envelope signature verification
against a confirmed key — a stronger property, but it also means an
unauthenticated peer can occupy a connection slot.Protocol and product
Known-unfixed operational notes
Ordered by what would most increase confidence, not by what would look best.
DEVICE_TESTING.md with three or more
devices and record the result. This is the single largest gap.Issues and pull requests are welcome, and responsible bug reports are especially valuable — particularly about routing correctness, message integrity, or security.
Start with CONTRIBUTING.md for setup, test commands, evidence expectations, and pull request guidance. Participation is governed by CODE_OF_CONDUCT.md. Report vulnerabilities privately as described in SECURITY.md, never in a public issue.
Read docs/security/threat-model.md before proposing a change to the trust or transport model.
When you report a finding, say honestly whether you reproduced it, read it, or inferred it. Several defects here were found by reading the code rather than by running it, and the difference between a reproduced and an inferred report matters a great deal to whoever triages it.
If you have tested the Android app on real hardware, that result is worth more than anything else you could send. The procedure is in apps/android/DEVICE_TESTING.md.
Please keep the "what is verified / what is simulated / what is untested" distinction intact in any change. It is the most valuable thing in this repository, and it is easy to erode by accident.
MIT. See LICENSE.