>_ case study

Mesh App

EU-sovereign decentralized mesh network for offline-first communication. When cell towers go dark, Mesh keeps people connected through peer-to-peer WiFi Direct and Bluetooth Low Energy — with military-grade encryption and zero cloud dependency.

Mobile | Expo / React NativeAndroid | Kotlin + WiFi Direct + BLEiOS | Swift + CoreBluetoothCrypto | X25519 + XChaCha20-Poly1305Storage | SQLite (expo-sqlite)PWA | Vite + Service WorkersBuild | Turborepo + Bun + EASLicense | EUPL-1.2

>_The Problem

Natural disasters, infrastructure failures, and conflict zones share a common trait: traditional communication fails first. Cell towers go offline, internet links saturate, centralized services become unreachable.

Existing alternatives either require specialized hardware (LoRa, satellite phones), depend on cloud infrastructure (Signal, WhatsApp), or sacrifice privacy for connectivity. There was no phone-to-phone, zero-infrastructure, encrypted messaging solution that works on standard consumer devices.

Mesh fills that gap. Built during the EU Critical Infrastructure Hackathon (where it received an Honorable Mention), it turns every smartphone into a mesh relay node — no SIM, no Wi-Fi, no servers required.

>_Architecture

Turborepo monorepo with 3 packages and 2 apps. Each layer has a single responsibility — protocol logic never touches native Bluetooth, and native modules never touch UI.

monorepo structure
mesh-app/ ├── packages/ │ ├── mesh-core protocol + crypto interface │ ├── mesh-android WiFi Direct + BLE (Kotlin) │ └── mesh-ios CoreBluetooth (Swift) ├── apps/ │ ├── mobile Expo Router + RN bridge │ └── pwa Vite gateway + relay └── turbo.json task orchestration
📦

mesh-core

885 LOC

Shared protocol layer. Defines MeshMessage type with TTL routing, Ed25519 signatures, encryption provider interface, and the relay decision algorithm. Platform-agnostic TypeScript.

📶

mesh-android

1,571 LOC

Kotlin native module with dual transport: WiFi Direct (ServerSocket on port 8888, P2P group negotiation) and BLE (GATT service 6E400001, central + peripheral roles, 512-byte MTU). Exposes React Native bridge via MeshAndroidModule.

🍏

mesh-ios

999 LOC

Swift native module using CoreBluetooth exclusively (iOS lacks WiFi Direct). Implements CBCentralManager for scanning and CBPeripheralManager for advertising. Same GATT service UUIDs as Android for cross-platform interop.

📱

mobile app

6,161 LOC

Expo Router app with platform abstraction layer. MeshNetwork.ts bridges to native modules, SecureMessaging.ts handles encryption, MessageQueue.ts manages offline retry with exponential backoff (1s → 5m, 5 attempts max).

🌐

pwa gateway

975 LOC

Vite-based Progressive Web App acting as internet gateway. WebSocket relay to EU infrastructure endpoint. Persists last 100 messages in localStorage. Emergency dispatch protocol for government channels.

>_End-to-End Encryption

Every message uses ephemeral key exchange for forward secrecy. No private key ever leaves the device. No server ever sees plaintext.

1

Key Generation

Each device generates Ed25519 (signing) and X25519 (encryption) key pairs from 32-byte random seeds. Identity fingerprint = first 8 bytes of SHA-256(publicKey) as hex.

2

Ephemeral ECDH

Per-message: generate fresh X25519 ephemeral keypair. Compute shared secret via ECDH(ephemeralSecret, recipientPublicKey). Forward secrecy — compromising long-term keys can't decrypt past messages.

3

Symmetric Encryption

XChaCha20-Poly1305 with 24-byte random nonce. Fallback chain: XChaCha20 → ChaCha20 → AES-256-GCM. Authenticated encryption prevents tampering.

4

Signature

Entire unsigned message JSON is signed with sender's Ed25519 key. Recipients verify signature against known peer registry before processing.

5

Wire Format

[ephemeralKey | nonce | ciphertext | auth_tag] — everything needed for the recipient to decrypt without prior key exchange.

wire format
[ephemeralKey 32B | nonce 24B | ciphertext variable | auth_tag 16B]

>_Mesh Routing

Store-and-forward with TTL-based flooding, two-layer deduplication, and priority-scored relay queues.

🔄

TTL-Based Flooding

Default TTL = 10 hops. Each relay decrements TTL and appends self to route array. Messages drop at TTL 0. With BLE ~100m range, theoretical reach is ~1km.

🚫

Loop Prevention

Route array stores every node that touched the message. If currentNode is already in route[], message is dropped. Combined with in-memory Set of 1,000 recent message IDs.

💾

Persistent Dedup

message_routes SQLite table persists [messageId, route[], ttl, received_at] across app restarts. Two-layer strategy: fast in-memory check, durable DB fallback.

Priority Queue

Messages scored: +10 for direct (vs broadcast), -ageMinutes (newer = higher), +remainingTTL (less relayed = higher). Direct messages always win.

📤

Offline Queue

MessageQueue.ts holds pending messages with exponential backoff: 1s, 5s, 30s, 1m, 5m. Immediate flush when recipient peer connects. 7-day TTL on queued messages.

>_Platform Strategy

Android and iOS have fundamentally different P2P capabilities. The architecture accounts for this asymmetry.

📶Android

  • WiFi Direct (primary) — 250 Mbps, ~200m range, group owner negotiation
  • BLE (fallback) — lower power, ~100m, simultaneous central + peripheral
  • Kotlin + Coroutines, StateFlow for reactive state, WifiP2pManager API
  • Both transports run concurrently — WiFi Direct for throughput, BLE for discovery

🍏iOS

  • BLE only — Apple restricts WiFi Direct API access for third-party apps
  • CoreBluetooth with dedicated DispatchQueue (.userInitiated QoS)
  • Dual role: CBCentralManager (scanner) + CBPeripheralManager (advertiser)
  • Same GATT service UUID as Android — cross-platform BLE interop out of the box

🌐PWA Gateway

The PWA acts as a bridge between the mesh and the internet. When a PWA node has connectivity, it can relay mesh messages to a WebSocket gateway (wss://eu-mesh-gateway.europa.eu/ws) and dispatch emergency messages to government channels. Relaying is opt-in per message via the allowExternalRelay flag — messages stay local by default.

>_Offline-First Design

No internet required at any point. Keys, contacts, messages, and routing tables live in on-device SQLite.

sqlite schema (8 tables)
identity ........... signing + encryption keys (singleton) contacts ........... known peers + public keys + verification conversations ....... 1:1 or group, unread count, last activity messages ........... content + encrypted payload + status message_routes ..... dedup: route[], ttl, received_at peers .............. discovery cache: signal, hops, via conversation_participants indexes on conversation_id, timestamp, last_seen

Message Lifecycle

pendingsendingsentdeliveredfailed

Failed messages retry with exponential backoff: 1s, 5s, 30s, 1m, 5m. When a disconnected peer reconnects, their pending queue flushes immediately. Messages older than 7 days are cleaned up automatically.

>_By the Numbers

13,139

Lines of Code

692

Source Files

5

Apps & Packages

8

SQLite Tables

10

Max TTL Hops

24

Byte Nonce Size

512

BLE MTU Bytes

8888

WiFi Direct Port

>_Testing

End-to-end tests run on physical Android devices using WebdriverIO + Appium with UiAutomator2. Two devices communicate over BLE to verify:

BLE peer discovery (<5 sec)
Nickname broadcast + persistence
Direct message delivery
ACK delivery (✓ → ✓✓)
Bidirectional cross-device messaging
← back to raba.plmesh.com.pl →