Files
kosmo-connect/docs/architecture/messaging-gateway.md
Tomas Kracmar 0a4fb7b55e
Some checks failed
CI / lint-docs (push) Has been cancelled
CI / build-firmware (push) Has been cancelled
CI / test-backend (push) Has been cancelled
CI / test-web (push) Has been cancelled
feat: initial KosmoConnect platform v0.1
Includes:
- Backend services: ingestion (:8001), weather API (:8002),
  gateway (:8003), billing (:8004) with BTCPay integration
- Shared asyncpg pool, TimescaleDB hypertable, Redis, Mosquitto MQTT
- React frontend: Dashboard (MapLibre) and Messaging (chat UI)
- Bridge daemon for Pi + Meshtastic (Serial/TCP T-Deck support)
- Production Docker Compose, Nginx reverse proxy, ops scripts
- DEPLOY.md with step-by-step deployment guide
2026-04-12 17:30:15 +02:00

6.2 KiB

Messaging Gateway Architecture

The Messaging Gateway is the bridge between the internet (web users) and the Meshtastic mesh. It is the primary monetization surface for the project.

Business Rules

  1. The mesh is open: Anyone with a Meshtastic device can join the Kosmo mesh and send/receive messages locally for free.
  2. The gateway is gated: Sending a message from the internet to the mesh requires an active subscription.
  3. Authorization granularity:
    • Network-level: Subscriber can send to any node reachable through the gateway.
    • Node-level: Subscriber can send only to specific whitelisted nodes (e.g., family members).
    • Future: Group-level access for organizations.

Gateway Flow: Web → Mesh

User (Web Browser)
       │
       ▼
┌──────────────┐
│   Web API    │  <-- Validates JWT, checks subscription status
│   /messages  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Billing    │  <-- Confirms subscriber has active plan & quota remaining
│    Service   │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Message    │  <-- Writes message to outbound queue (RabbitMQ / Redis)
│   Gateway    │      Topic: `mesh.outbound.{node_id}`
└──────┬───────┘
       │ MQTT / TLS
       ▼
┌──────────────┐
│ Infrastructure│ <-- Bridge daemon reads queue
│    Node       │
└──────┬───────┘
       │ Serial / protobuf API
       ▼
┌──────────────┐
│  Meshtastic   │ <-- Broadcasts text message to target node ID
│    Radio      │
└───────────────┘

Gateway Flow: Mesh → Web

Replies and inbound messages from the mesh to a subscriber:

Meshtastic Radio (any node)
       │
       ▼
┌──────────────┐
│ Infrastructure│ <-- Receives mesh message
│    Node       │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Bridge     │ <-- Publishes to `kosmo/mesh/inbound`
│   Daemon     │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Message    │ <-- Matches sender node ID to subscriber inboxes
│   Gateway    │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Web API    │ <-- Stores in user's inbox, sends push notification
│   Inbox      │
└───────────────┘

Subscription Models

Plan Tiers (Example)

Tier Price Messages/Month Scope Features
Free $0 5 (inbound only) Inbox Receive replies, view weather
Wanderer $5/mo 50 Network Send to any node
Guardian $12/mo 500 Node-level Manage up to 5 linked nodes
Sanctuary $50/mo Unlimited Network + API Bulk messaging, webhook access

All paid plans are processed through the Church of Kosmo's self-hosted BTCPay Server at pay.cqre.net.

Authorization Check

When a user attempts to send a message:

def can_send(user: User, target_node_id: str) -> bool:
    subscription = user.active_subscription()
    if not subscription or subscription.is_expired():
        return False
    
    if subscription.plan == "network":
        return True
    
    if subscription.plan == "node_level":
        return user.allowed_nodes.filter(mesh_node_id=target_node_id).exists()
    
    return False

Message Queue Schema

Outbound (Cloud → Mesh)

{
  "message_id": "uuid-v4",
  "sender_user_id": "uuid-v4",
  "target_node_id": "!a1b2c3d4",
  "text": "Hello from the web!",
  "priority": "normal",
  "max_hops": 7,
  "want_ack": true,
  "created_at": "2026-04-12T09:20:00Z",
  "retry_count": 0
}

Inbound (Mesh → Cloud)

{
  "message_id": "uuid-v4",
  "source_node_id": "!a1b2c3d4",
  "gateway_node_id": "!gateway01",
  "text": "Reply from the woods",
  "hop_count": 3,
  "rssi": -90,
  "snr": 8.5,
  "received_at": "2026-04-12T09:25:00Z"
}

Rate Limiting & Anti-Spam

  • Per-user: Max 1 message per 10 seconds, burst of 5
  • Per-subscription tier: Enforced monthly quotas
  • Per-target-node: Max 10 web messages per hour (to prevent harassment)
  • Content filtering: Basic profanity/spam filter on the gateway
  • Blocklist: Users and nodes can block each other

Delivery Tracking

The gateway tracks message state:

PENDING -> QUEUED -> TRANSMITTED -> DELIVERED (ACK received)
                          |
                          +-> FAILED (max retries exceeded)

Users see delivery status in the messaging UI:

  • Single checkmark: Queued
  • Double checkmark: Transmitted by gateway
  • Blue double checkmark: Delivered (ACK from target node)

Billing Integration

The gateway relies on the Billing Service to enforce subscriptions. The billing service:

  • Creates invoices via BTCPay Server Greenfield API
  • Listens to BTCPay webhooks for payment confirmation
  • Manages subscription validity periods and quotas in PostgreSQL
  • Deactivates old subscriptions and resets quotas on successful payment

Security Considerations

  1. Authentication: JWT-based auth for web users, API keys for bridge daemons
  2. Encryption: Mesh messages are encrypted with the channel key. The bridge daemon does not decrypt content; it only forwards the encrypted payload.
  3. Privacy: The gateway logs message metadata (sender, recipient, timestamp, size) but does not log message content.
  4. Node Impersonation: Web messages are tagged with a special prefix or sender ID indicating they originated from the gateway, preventing spoofing of local mesh nodes.

Fallback Behavior

If no infrastructure node is currently online:

  • Outbound messages remain queued for up to 24 hours
  • Users are notified that delivery is delayed
  • If the queue expires, the message is marked as failed and the user's quota is refunded