Decentralized Offline Emergency Communication Network
CrisisMesh is a battle-tested, decentralized mesh communication system designed for disaster response, emergency coordination, and offline scenarios. Built with Go, it provides two distinct interfaces: a Terminal UI (TUI) for commanders and coordinators, and a mobile-first Web UI for field workers and civilians—all operating without internet connectivity.
- Overview
- Key Features
- Architecture
- Getting Started
- Usage Guide
- Emergency Protocol
- Security & Encryption
- Testing & Examples
- Configuration
- Troubleshooting
- Advanced Topics
- Development
- License
CrisisMesh enables offline, serverless communication in scenarios where traditional infrastructure has failed. Devices form a self-organizing mesh over local Wi-Fi, hotspots, or Ethernet. Messages are delivered using a store-and-forward approach inspired by Delay Tolerant Networking (DTN), ensuring eventual delivery even when sender and recipient are never online simultaneously.
- Disaster Response: Coordinate rescue teams after earthquakes, floods, or hurricanes
- Emergency Services: Medical teams communicating in areas with infrastructure damage
- Remote Operations: Military or exploration teams in off-grid environments
- Community Networks: Grassroots communication during internet shutdowns
- Field Research: Scientists collaborating in remote locations
- Offline-First: Works completely disconnected from the internet
- Zero Configuration: Auto-discovery via UDP broadcasts, no manual setup
- Dual Interface: TUI for monitoring/coordination, Web UI for mobile/field use
- Store-and-Forward: Messages persist until delivered, resilient to network partitions
- Security-Aware: End-to-end encryption for direct messages using NaCl
- Automatic Peer Discovery: UDP broadcast heartbeats (1-second interval)
- Full Mesh Topology: Each node connects to all discovered peers via TCP
- Gossip Protocol: Message inventory sync every 5 seconds (SYNC/REQ packets)
- Auto-Reconnect: Automatic TCP reconnection when stale peers reappear
- Store-and-Forward: Messages queued locally until recipient comes online
- Split-Screen Layout: 70% message stream + 30% peer monitor
- Real-Time Monitoring: Live peer table with connection status
- Visual Alerts: Pulsing red border for emergency SOS messages
- Keyboard Shortcuts:
Q: Show QR code for mobile onboardingM: Toggle monitor mode (JSON logs)Ctrl+S: Broadcast "SAFE ALERT: I am safe!"?: Help overlay
- Read-Only: Focus on monitoring and awareness (v0.2.0 will add input)
- Mobile-First Design: Chat bubble interface optimized for phones
- SOS Button: Red circular button with GPS acquisition
- Network Visualization: Force-directed graph showing mesh topology
- Identity Management: localStorage-based identity (no accounts)
- Polling Updates: 1-second refresh for new messages
- Responsive: Works on smartphones, tablets, and desktops
- Priority System: Normal (Priority 1) vs Emergency (Priority 2)
- SOS Detection: Auto-detects "SOS" keyword → Priority 2
- GPS Integration: Browser Geolocation API with Tokyo fallback
- Visual Alerts: Pulsing red borders, alert styling
- Cloud Uplink: Optional Discord webhook relay for SOS messages
- End-to-End Encryption: NaCl sealed box (XSalsa20-Poly1305 + Curve25519)
- Direct Messages:
/dm <nickname> <message>command - Identity System: Per-node UUID + keypair in JSON file
- Message Deduplication: SHA256-based message IDs prevent loops
- No Central Authority: Fully decentralized, no single point of failure
- SQLite Database: WAL mode for concurrent access
- GORM ORM: Clean database abstraction layer
- Message History: All sent/received messages with metadata
- Peer Records: Discovered peers with last-seen timestamps
- GPS Coordinates: Stored with Priority 2 messages
┌─────────────────────────────────────────────────────────────┐
│ CrisisMesh Node │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Commander UI │ │ Civilian UI │ │
│ │ (TUI) │ │ (Web UI) │ │
│ │ Bubble Tea │ │ HTTP + JS │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Gossip Engine │ │
│ │ (Message Routing) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ┌──▼────┐ ┌─────▼─────┐ ┌───▼────┐ │
│ │ UDP │ │ TCP │ │ SQLite │ │
│ │ Disco │ │ Mesh │ │ DB │ │
│ │ very │ │ Transport │ │ (WAL) │ │
│ └───────┘ └───────────┘ └────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│ │ │
│ Heartbeat │ MSG/SYNC/REQ │ Optional
│ (1s) │ (TCP Mesh) │ Uplink
▼ ▼ ▼
255.255.255.255 Peer Nodes Discord Webhook
- Broadcast: 255.255.255.255 on configured port
- Frequency: 1 heartbeat per second
- Payload: JSON with node ID, nickname, port, public key, timestamp
- Framing: 4-byte big-endian length prefix + JSON payload
- Max Payload: 10MB (prevents memory exhaustion)
- Packets: MSG (messages), SYNC (inventory), REQ (requests)
- Gossip Protocol: Epidemic-style message propagation
- Message ID: 16-char hex (first 64 bits of SHA256)
- TTL: 10 hops maximum before discard
- Deduplication: Database-backed message ID tracking
User Input (TUI/Web)
→ Gossip Engine (PublishText)
→ SQLite (SaveMessage)
→ TCP Broadcast (to all peers)
→ Peer Gossip Engines
→ Peer Databases
→ Peer UIs
- Go: 1.24 or higher
- GCC: Required for SQLite CGO compilation
- OS: Linux, macOS, or Windows (via WSL2)
# Clone the repository
git clone https://github.com/bit2swaz/crisismesh.git
cd crisismesh
# Download dependencies
go mod download
# Build the binary
go build -o crisis ./cmd/crisis
# Verify build
./crisis --help# Start with TUI (Commander View)
./crisis start --nick COMMAND --port 9000
# The TUI will open immediately
# Web UI available at http://localhost:10000 (port + 1000)# Terminal 1: Alice (Commander)
./crisis start --nick ALICE --port 9000
# Terminal 2: Bob (Field Agent)
./crisis start --nick BOB --port 9001
# Terminal 3: Charlie (Civilian)
./crisis start --nick CHARLIE --port 9002
# They will discover each other within 2 seconds
# Web UIs: http://localhost:10000, 10001, 10002# 1. Start node on laptop with Wi-Fi hotspot enabled
./crisis start --nick GATEWAY --port 9000
# 2. Press 'Q' in TUI to show QR code
# 3. Scan QR code with mobile phone camera
# 4. Phone browser opens Web UI automatically
# 5. Enter identity (e.g., "FIELD01")
# 6. Start messaging!The Terminal UI is designed for coordinators and command centers who need to monitor all activity, track peers, and maintain situational awareness.
╔══════════════════════════════════════════════════════════════════╗
║ CrisisMesh v0.1.2 │ Node: abc123 │ Peers: 3 │ Uptime: 5m ║
╠════════════════════════════════════╦═════════════════════════════╣
║ ║ CONNECTED PEERS ║
║ MESSAGE STREAM (70%) ║ ┌──────────────────────┐ ║
║ ┌──────────────────────────────┐ ║ │ ALICE | 192.168.1.10 │ ║
║ │ [10:30] ALICE → All │ ║ │ BOB | 192.168.1.11 │ ║
║ │ Hello mesh │ ║ │ CAROL | 192.168.1.12 │ ║
║ │ │ ║ └──────────────────────┘ ║
║ │ [10:31] BOB → All [ENC] │ ║ ║
║ │ Testing encryption │ ║ Last Heartbeat: ║
║ │ │ ║ ALICE: 1s ago ║
║ │ [10:32] CAROL → All [P2] │ ║ BOB: 2s ago ║
║ │ PRIORITY ALERT: SOS │ ║ CAROL: 1s ago ║
║ │ [GPS: 35.6895, 139.6917] │ ║ ║
║ │ ◀── RED BORDER PULSING │ ║ Mesh Status: HEALTHY ║
║ └──────────────────────────────┘ ║ Messages: 127 total ║
║ ║ Storage: 2.3 MB ║
╠════════════════════════════════════╩═════════════════════════════╣
║ Status: ONLINE │ Latency: 45ms │ Press ? for help ║
╚══════════════════════════════════════════════════════════════════╝
| Key | Action |
|---|---|
Q |
Toggle fullscreen QR code (for mobile onboarding) |
M |
Toggle monitor mode (compact JSON log format) |
Ctrl+S |
Broadcast "SAFE ALERT: I am safe!" (Priority 2) |
? |
Toggle help overlay |
Ctrl+C |
Exit application |
[HH:MM] NICKNAME → All/DM [FLAGS]
Message content here
[GPS: lat, long] (if Priority 2)
FLAGS:
[ENC] = Encrypted direct message
[P2] = Priority 2 (emergency)
[HOP:N] = Multi-hop (N hops traversed)
- Pulsing Red Border: Full-screen border alternates between bright red (#FF0000) and dark red (#550000) every 500ms when Priority 2 message received
- Yellow GPS Tags: GPS coordinates displayed in yellow text
- Red "NO SIGNAL": Displayed when Priority 2 but no GPS coordinates
The Web UI is designed for field workers, volunteers, and civilians who need simple, mobile-friendly messaging capabilities.
┌─────────────────────────────────────┐
│ CrisisMesh [≡] [SOS] │ ← Header (fixed)
├─────────────────────────────────────┤
│ │
│ ┌────────────────────┐ │
│ │ Alice: Hello! │ 10:30 │ ← Chat bubbles
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ 10:31 │ Testing 123 │ │ ← Your messages
│ │ You │ │
│ └────────────────────┘ │
│ │
│ ┌────────────────────┐ │
│ │ ⚠️ PRIORITY ALERT │ 10:32 │ ← SOS message
│ │ Bob: SOS │ │
│ │ 📍 35.6895, 139.69 │ │
│ └────────────────────┘ │
│ │
├─────────────────────────────────────┤
│ [Type message...] [Send] │ ← Input (fixed)
└─────────────────────────────────────┘
-
Messages (
/or/messages.html)- Chat interface with message bubbles
- Scroll to view history
- Send broadcasts and DMs
- Red SOS button (top-right)
-
Network (
/network.html)- Force-directed graph visualization
- Green nodes = active peers
- Gray nodes = stale peers
- "ME" node highlighted
-
Settings (
/settings.html)- View/change identity
- Network status
- Clear localStorage
The prominent red circular button in the top-right corner triggers emergency broadcasts:
- Click SOS button
- Confirm dialog appears
- Browser requests GPS permission
- GPS acquired (5-second timeout)
- Sends "PRIORITY ALERT: SOS" with coordinates
- All commanders see pulsing red border
- Optional: Discord webhook relays to cloud
| Method | Interface | Content | GPS |
|---|---|---|---|
| Red SOS button | Web UI | "PRIORITY ALERT: SOS" | Auto-acquired |
| Type "SOS" | Web UI | "PRIORITY ALERT: SOS" | If available |
Ctrl+S hotkey |
TUI | "SAFE ALERT: I am safe!" | Display only |
CrisisMesh automatically detects the keyword "SOS" (case-insensitive) and upgrades to Priority 2:
// Automatic priority upgrade
if strings.ToUpper(content) == "SOS" {
priority = 2
content = "PRIORITY ALERT: SOS"
}Primary Method:
- Browser Geolocation API (
navigator.geolocation.getCurrentPosition) - High accuracy mode enabled
- 5-second timeout
- Maximum age: 0 (force fresh fix)
Fallback (Simulation):
- Base coordinates: Tokyo Station (35.689487, 139.691706)
- Random jitter: ±0.001° (~110 meters)
- Used when: Permission denied, timeout, or API unavailable
If --discord-webhook flag provided, Priority 2 messages are automatically relayed to Discord:
./crisis start --nick COMMAND --port 9000 \
--discord-webhook https://discord.com/api/webhooks/123/abcDiscord Message Format:
📡 **[MESH RELAY]**
**User:** ALICE
**Message:** PRIORITY ALERT: SOS
**Location:** 35.6895, 139.6917
[Open in Maps](https://maps.google.com/?q=35.6895,139.6917)
Uplink Behavior:
- Non-blocking: Uses buffered channel (size 100)
- Filters: Only Priority 2 OR messages starting with
/uplink - Async: Runs in separate goroutine, never blocks mesh
- Confirmation: Stdout prints "[UPLINK] Relayed to Cloud"
Command Syntax:
/dm <nickname> <message>
Example:
/dm ALICE Meet at extraction point B
Encryption Process:
- Lookup recipient's public key in peers table
- Encrypt plaintext with NaCl sealed box
- Store plaintext locally (sender can read own messages)
- Broadcast hex-encoded ciphertext over mesh
- Recipient automatically decrypts with private key
- Recipient stores decrypted plaintext
Algorithm:
- Cipher: XSalsa20-Poly1305 (authenticated encryption)
- Key Exchange: Curve25519 ECDH
- Mode: Anonymous sealed box (only recipient's public key needed)
- Key Size: 32 bytes (256 bits)
Each node has a unique identity stored in JSON:
{
"node_id": "550e8400-e29b-41d4-a716-446655440000",
"nickname": "ALICE",
"private_key": "hex-encoded 32-byte NaCl key",
"public_key": "hex-encoded 32-byte NaCl key"
}Files:
identity.json- Default identity (auto-created)identity_9000.json- Port-specific (for multi-node testing)
Key Generation:
- Curve25519 keypair generated on first run
- Private key NEVER leaves the local machine
- Public key broadcast in UDP heartbeats
- Loss of private key = loss of ability to decrypt old DMs
Current (v0.1.2):
- ❌ Broadcast messages are NOT encrypted (plaintext on network)
- ❌ No message signing (authenticity not verified)
- ❌ No peer authentication (trust-on-first-use)
- ✅ Direct messages use E2E encryption
- ✅ Message replay prevented (timestamp in message ID)
Planned (v0.3.0):
- Ed25519 message signing for broadcast authenticity
- Group key exchange for encrypted broadcasts
- Peer reputation system (Sybil resistance)
# Terminal 1
./crisis start --nick ALICE --port 9000
# Terminal 2
./crisis start --nick BOB --port 9001
# Expected: Both TUIs show each other in peer table within 2 seconds# Terminal 1: Alice
./crisis start --nick ALICE --port 9000
# Terminal 2: Bob (start, then STOP)
./crisis start --nick BOB --port 9001
# Let them connect, then press Ctrl+C to stop Bob
# Terminal 3: Send message to Bob via Web UI
curl -X POST http://localhost:10000/api/messages \
-H "Content-Type: application/json" \
-d '{"content":"Hello Bob","author":"ALICE"}'
# Terminal 2: Restart Bob
./crisis start --nick BOB --port 9001
# Expected: Bob receives "Hello Bob" within 5 seconds (next gossip sync)# Terminal 1: Start with Discord webhook
./crisis start --nick COMMAND --port 9000 \
--discord-webhook https://discord.com/api/webhooks/YOUR_WEBHOOK
# Browser: Open http://localhost:10000
# 1. Enter identity (e.g., "FIELD01")
# 2. Click red SOS button
# 3. Confirm dialog
# 4. Allow GPS permission
# Expected in TUI:
# - Pulsing red border
# - Message: [USER: FIELD01] → PRIORITY ALERT: SOS [GPS: x.xxxx, y.yyyy]
# - Stdout: "[UPLINK] Relayed to Cloud"
# Expected in Discord:
# - Message with Google Maps link# Terminal 1: Alice
./crisis start --nick ALICE --port 9000
# Terminal 2: Bob
./crisis start --nick BOB --port 9001
# Wait for peer discovery (2 seconds)
# Send encrypted DM via Web UI:
curl -X POST http://localhost:10000/api/messages \
-H "Content-Type: application/json" \
-d '{"content":"/dm BOB This is secret","author":"ALICE"}'
# Expected:
# - Alice's TUI: Shows plaintext (or encrypted if read-only)
# - Bob's TUI: Shows decrypted "[USER: ALICE] → This is secret"
# - Network: Hex-encoded ciphertext# Terminal 1: Alice
./crisis start --nick ALICE --port 9000
# Terminal 2: Bob
./crisis start --nick BOB --port 9001
# Terminal 3: Charlie
./crisis start --nick CHARLIE --port 9002
# All three connect, observe peer tables
# Kill Bob (Ctrl+C), wait 15 seconds
# Alice and Charlie mark Bob as stale
# Restart Bob
./crisis start --nick BOB --port 9001
# Expected:
# - Within 2 seconds: Bob reappears in Alice and Charlie's peer tables
# - Automatic SYNC triggered
# - Queued messages delivered./crisis start [flags]
Required:
--nick <string> User nickname (e.g., "COMMAND", "ALPHA")
Optional:
--port <int> Mesh + discovery port (default: 9000)
--web-port <int> Web UI port (default: <port> + 1000)
--discord-webhook <url> Discord webhook for SOS relay# Commander with default ports
./crisis start --nick COMMAND --port 9000
# Web UI at http://localhost:10000
# Custom web port
./crisis start --nick ALICE --port 9001 --web-port 8080
# Web UI at http://localhost:8080
# With Discord uplink
./crisis start --nick GATEWAY --port 9000 \
--discord-webhook https://discord.com/api/webhooks/123/abc
# Headless mode (Web UI only, no TUI)
CRISIS_HEADLESS=true ./crisis start --nick SERVER --port 9000CRISIS_HEADLESS=true # Disable TUI, run Web UI only
DEBUG=true # Enable verbose debug logging.
├── crisis # Binary
├── identity.json # Default identity
├── identity_9000.json # Per-port identities
├── identity_9001.json
├── crisis.db # Default database
├── crisis_9000.db # Per-port databases
└── crisis_9001.db
Default Behavior:
- Gossip port: 9000 (TCP + UDP)
- Web port: 10000 (gossip port + 1000)
Multi-Node Testing:
# Automatic web port calculation
./crisis --port 9000 # Web: 10000
./crisis --port 9001 # Web: 10001
./crisis --port 9002 # Web: 10002Messages Table:
id- 16-char hex message IDsender_id- Node UUIDrecipient_id- Target UUID (empty = broadcast)content- Plaintext or ciphertextpriority- 1 (normal) or 2 (SOS)author- Human-readable nicknamelat,long- GPS coordinatestimestamp- Unix secondsttl- Remaining hopshop_count- Hops traversedstatus- "sent", "delivered", "failed"is_encrypted- Boolean flag
Peers Table:
id- Node UUIDnickname- Display namepublic_key- Hex-encoded NaCl keylast_seen- Timestampport- TCP listening port
Symptom: TUI peer table shows 0 peers
Solutions:
- Check firewall: Allow UDP broadcast on configured port
- Verify subnet: Nodes must be on same LAN (192.168.x.x)
- Check logs: Look for "Heartbeat sent" messages
- Test manually:
/connect <ip>:<port>(not yet exposed in TUI)
Symptom: Sent messages don't appear on recipient
Solutions:
- Check peer table: Recipient must be in "Peers" list
- Wait for sync: Gossip syncs every 5 seconds
- Check TTL: Messages expire after 10 hops
- Verify database:
sqlite3 crisis.db "SELECT * FROM messages;"
Symptom: SOS shows "NO SIGNAL" or Tokyo coordinates
Solutions:
- Use HTTPS: Geolocation API requires HTTPS or localhost
- Allow permission: Browser must have location permission
- Wait longer: GPS acquisition can take 5+ seconds outdoors
- Fallback is normal: Tokyo coordinates indicate GPS timeout (not an error)
Symptom: Browser shows "Cannot connect"
Solutions:
- Check port: Default is
<port> + 1000(e.g., 9000 → 10000) - Check firewall: Allow HTTP on web port
- Try localhost: Use
http://localhost:10000not127.0.0.1 - Check logs: Look for "HTTP server started on :10000"
Symptom: Garbled text, broken borders
Solutions:
- Update terminal: Use modern terminal (iTerm2, Windows Terminal, etc.)
- Set TERM:
export TERM=xterm-256color - Increase size: TUI requires minimum 80x24 characters
- Check font: Use monospace font with good Unicode support
func generateMessageID(senderID, content string, timestamp int64) string {
h := sha256.New()
h.Write([]byte(senderID))
h.Write([]byte(content))
h.Write([]byte(fmt.Sprintf("%d", timestamp)))
return hex.EncodeToString(h.Sum(nil))[:16] // First 16 hex chars
}Properties:
- Deterministic: Same inputs = same ID
- Unique: Different sender/content/time = different ID
- Collision-resistant: ~2^-64 probability
- Prevents replay: Timestamp ensures uniqueness
SYNC Phase (every 5 seconds):
- Node A sends list of all message IDs to Node B
- Node B compares against local database
- Node B identifies missing IDs
REQ Phase:
- Node B sends list of missing IDs to Node A
- Node A looks up full messages for those IDs
- Node A sends MSG packets with full payloads
Result: Eventually consistent message history across all nodes
┌────────────────────────┐
│ Length (4 bytes) │ Big-endian uint32
├────────────────────────┤
│ Payload (N bytes) │ JSON-encoded packet
└────────────────────────┘
Max payload: 10MB (prevents DoS)
- MSG: Full message with metadata
- SYNC: Array of message IDs
- REQ: Array of requested message IDs
Full Mesh (N nodes):
- Connections: N * (N-1) / 2
- Example: 5 nodes = 10 connections
- Scales poorly beyond 20-30 nodes
Future: Implement smart routing (OLSR, BATMAN) for larger networks
crisismesh/
├── cmd/
│ └── crisis/
│ ├── main.go # Entry point
│ └── root.go # Cobra CLI setup
├── internal/
│ ├── config/
│ │ └── config.go # Config structs
│ ├── core/
│ │ └── identity.go # Keypair + UUID generation
│ ├── discovery/
│ │ └── heartbeat.go # UDP broadcast
│ ├── engine/
│ │ ├── gossip.go # Message routing
│ │ └── handlers.go # Packet handlers
│ ├── logger/
│ │ └── logger.go # Structured logging
│ ├── protocol/
│ │ └── types.go # Packet definitions
│ ├── store/
│ │ ├── db.go # Database layer
│ │ └── models.go # GORM models
│ ├── transport/
│ │ ├── framing.go # TCP framing
│ │ └── manager.go # Connection pool
│ └── tui/
│ ├── model.go # Bubble Tea model
│ └── view.go # Rendering
├── static/ # Web UI files
├── go.mod
└── README.md
# Standard build
go build -o crisis ./cmd/crisis
# With race detector
go build -race -o crisis ./cmd/crisis
# Optimized for size
go build -ldflags="-s -w" -o crisis ./cmd/crisis
# Cross-compile for Raspberry Pi
GOOS=linux GOARCH=arm64 go build -o crisis-arm64 ./cmd/crisis# Run unit tests
go test ./...
# Run with coverage
go test -cover ./...
# Specific package
go test ./internal/store/
# Verbose output
go test -v ./internal/engine/github.com/charmbracelet/bubbletea # TUI framework
github.com/charmbracelet/lipgloss # TUI styling
github.com/google/uuid # UUID generation
github.com/spf13/cobra # CLI framework
golang.org/x/crypto/nacl/box # NaCl encryption
gorm.io/gorm # ORM
gorm.io/driver/sqlite # SQLite driver
MIT License - see LICENSE file for details
Contributions welcome! Please see PRD_v0.1.2.md for detailed technical specifications.
Priority Areas:
- TUI message input widget (v0.2.0)
- Broadcast message encryption
- Message signing (Ed25519)
- Mobile app (React Native / Flutter)
- LoRa radio support
- Documentation: See
PRD_v0.1.2.mdfor comprehensive technical documentation - Repository: https://github.com/bit2swaz/crisismesh
- Issues: https://github.com/bit2swaz/crisismesh/issues
Built with ❤️ for disaster response and emergency communication
