Offline today. Connected always.
Nodex is an offline-first, peer-to-peer mesh communication app built for the hackathon challenge:
"How do we keep people connected when all networks go down? Build an offline mesh communication app where nearby phones relay messages directly between each other using Bluetooth or Wi-Fi Direct."
Every phone running Nodex is simultaneously a sender, a receiver, and a relay node. No internet. No cell towers. No servers. Just phones talking to phones — across a building, a disaster zone, or anywhere infrastructure fails.
Phone A ──BLE──► Phone B ──BLE──► Phone C
(sender) (relay) (receiver — out of A's range)
A sends a message. B relays it automatically. C receives it — with zero internet, zero cell signal, zero servers involved. That is the entire value proposition, and it works today.
| Feature | Status | Notes |
|---|---|---|
| BLE peer discovery | ✅ Done | Google Nearby Connections, P2P_CLUSTER strategy |
| Multi-hop message relay | ✅ Done | hopCount gate (max 5), excludeId flood prevention |
| Smart unicast routing | ✅ Done | Routing table populated via routing_adv packets |
| Message deduplication | ✅ Done | Hive seen_ids_box — survives app restart |
| Message persistence | ✅ Done | Hive messages_box with timestamp sort |
| Message sync on connect | ✅ Done | sync_req / sync_res protocol |
| Direct message encryption | ✅ Done | X25519 ECDH + AES-256-CTR |
| Cryptographic identity | ✅ Done | X25519 keypair generated once, stored in Hive |
| Human-readable alias | ✅ Done | Deterministic from public key — Silent-Blue-Fox-92 |
| Signal / latency tracking | ✅ Done | Ping/pong with exponential moving average |
| Background keep-alive | ✅ Done | flutter_background_service foreground notification |
| SOS broadcast | ✅ Done | MessageType.sos — floods mesh with TTL=5 |
| Global chamber | ✅ Done | recipientId = "global_chamber" — unencrypted broadcast |
| End-to-end encryption | ✅ Done | DM only — global chamber is plaintext by design |
┌─────────────────────────────────┐
│ UI Layer │ Flutter widgets, screens, navigation
├─────────────────────────────────┤
│ Routing Layer │ NearbyServiceController — dedup, TTL,
│ │ smart routing, ping, sync
├─────────────────────────────────┤
│ Transport Layer │ Google Nearby Connections API
│ │ Strategy.P2P_CLUSTER (BLE + Wi-Fi Direct)
├─────────────────────────────────┤
│ Storage Layer │ Hive — messages, seen IDs, keypair
└─────────────────────────────────┘
The routing layer is transport-agnostic. It does not know or care whether the underlying link is BLE or Wi-Fi Direct — swapping the transport requires changing only NearbyServiceController, not the rest of the app.
Every byte sent on the wire is a MeshPacket serialised as JSON:
{ "type": "chat", "data": { ...Message fields... } }| Packet type | Direction | Purpose |
|---|---|---|
chat |
broadcast / unicast | Carries a Message payload |
sync_req |
→ new peer | "Give me everything newer than timestamp T" |
sync_res |
→ requester | List of missing Message objects |
routing_adv |
broadcast | "I am reachable via this endpoint, hops=N" |
ping_req |
broadcast | Latency probe |
ping_res |
unicast reply | Latency response |
Message {
id : UUIDv4 // deduplication key
senderId : String // sender's X25519 public key (base64)
senderName : String // same as senderId (alias resolved in UI)
recipientId : String // target pubkey, or "global_chamber"
content : String // plaintext or AES-256-CTR ciphertext
timestamp : int // Unix ms — used for sync window
hopCount : int // incremented per relay, gate at 5
isEncrypted : bool
iv : String // base64 nonce for AES-CTR (DM only)
}| Threat | Mechanism | Status |
|---|---|---|
| Message tampering | X25519 ECDH shared secret + AES-256-CTR for DMs | ✅ |
| Relay loop / infinite flood | seen_ids_box dedup + hopCount ≤ 5 gate |
✅ |
| Replay attack | seen_ids_box persisted across sessions |
✅ |
| Identity spoofing | Identity = X25519 public key, not a username | ✅ |
| Global chamber eavesdropping | By design — global chamber is plaintext |
| Threat | Future mechanism |
|---|---|
| Global chamber privacy | Symmetric group key exchanged on join |
| Sybil attack (fake nodes) | Web-of-trust key signing, rate limiting per pubkey |
| Traffic analysis | Message padding to fixed size |
| MAC-less AES-CTR | Upgrade to AES-256-GCM for authenticated encryption |
Note on
MacAlgorithm.empty: The current AES-CTR implementation uses no MAC, meaning ciphertext integrity is not verified before decryption. This is a known, conscious hackathon trade-off. Upgrading to AES-GCM inCryptoServiceis a one-line change.
lib/
├── core/
│ ├── alias_generator.dart # Deterministic human alias from pubkey
│ ├── app_state.dart # (placeholder — global state future)
│ ├── background_service.dart # Foreground notification keep-alive
│ ├── crypto_service.dart # X25519 keygen + AES-256-CTR encrypt/decrypt
│ ├── database_service.dart # Hive wrapper — messages + seen IDs
│ ├── mesh_message.dart # Legacy wire format (MeshMessage — unused in v2)
│ ├── nearby_service_controller.dart # THE brain — all mesh logic lives here
│ └── permissions.dart # Android runtime permission orchestration
├── models/
│ ├── mesh_packet.dart # Wire envelope: { type, data }
│ └── message.dart # Domain model for a single message
├── features/
│ ├── home/ # Home screen — activity feed, live rooms
│ ├── nearby/ # Peer discovery and connection UI
│ ├── chat/ # DM chat screen with encryption
│ ├── messages/ # Conversation list screen
│ ├── sos/ # SOS broadcast screen
│ └── profile/ # Identity and settings
├── shared/
│ ├── app_shell.dart # Navigation shell + custom bottom nav
│ └── theme.dart # Design tokens
└── main.dart # Entry point — permissions → init → shell
- Flutter 3.x (stable channel)
- Android device or emulator with API 21+
- Two or more physical Android devices for mesh testing (emulators cannot do BLE)
git clone https://github.com/MedabisAmina/Nodex.git
cd Nodex
flutter pub get
flutter rundependencies:
nearby_connections: ^3.0.0
cryptography: ^2.7.0
hive_flutter: ^1.1.0
uuid: ^4.0.0
crypto: ^3.0.0 # for AliasGenerator SHA-256
flutter_background_service: ^5.0.0
flutter_local_notifications: ^17.0.0
permission_handler: ^11.0.0
location: ^6.0.0<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />MIT © 2026 Nodex Team
