Relay Protocol
This document describes the technical architecture of Crow's peer relay system — the store-and-forward layer that enables asynchronous delivery of Hypercore data between peers.
Overview
Crow's sharing system uses Hyperswarm for direct peer-to-peer connections and Hypercore for data replication. Both require peers to be online simultaneously. The relay protocol adds an intermediary that holds encrypted data for offline recipients.
Sender Relay Recipient
│ │ │
│ POST /relay/store │ │
│ {blob, signature} │ │
│────────────────────────>│ │
│ │ stores encrypted blob │
│ │ │
│ │ GET /relay/fetch │
│ │ {pubkey, signature} │
│ │<─────────────────────────│
│ │ │
│ │ {blobs: [...]} │
│ │─────────────────────────>│
│ │ │
│ │ deletes delivered blobs │Store-and-forward model
The relay acts as a temporary mailbox:
- Store — A sender pushes an encrypted blob addressed to a recipient's public key
- Hold — The relay stores the blob until the recipient fetches it or the TTL expires
- Forward — When the recipient connects and authenticates, all pending blobs are delivered
- Cleanup — Delivered blobs are deleted immediately; expired blobs are purged periodically
The relay never decrypts, inspects, or modifies blob contents. It is a dumb pipe with authentication.
Message format
Store request
POST /relay/store
Content-Type: application/json
{
"recipient": "<Ed25519 public key hex>",
"blob": "<encrypted data, base64 or JSON>",
"signature": "<Ed25519 signature hex>",
"senderPubkey": "<Ed25519 public key hex>"
}The signature covers JSON.stringify({ recipient, blob }), proving the sender authored the request.
The blob field contains data encrypted with NaCl box (Curve25519 + XSalsa20 + Poly1305) for the recipient's public key. The relay cannot decrypt it.
Fetch request
GET /relay/fetch?pubkey=<hex>&signature=<hex>×tamp=<ms>The signature covers "<pubkey>:<timestamp>". The timestamp must be within 5 minutes of the relay's clock to prevent replay attacks.
Fetch response
{
"blobs": [
{
"blob": "<encrypted data>",
"sender": "<sender Ed25519 pubkey hex>",
"timestamp": 1741782600000
}
],
"count": 1
}All matching blobs are returned in a single response and deleted from the relay.
Delivery guarantees
The relay protocol provides best-effort, eventual delivery:
- No guaranteed ordering — Blobs from multiple senders may arrive in any order
- At-most-once from relay — Once fetched, blobs are deleted; the relay does not re-deliver
- Eventual delivery via Hypercore — If relay delivery fails (TTL expires, relay goes down), Hypercore syncs the data directly when both peers are online via Hyperswarm
- No acknowledgment protocol — The sender does not receive confirmation that the recipient fetched the blob
The relay is an optimization layer, not the primary delivery mechanism. Hypercore's append-only feeds ensure data is never lost — the relay just accelerates delivery when peers have non-overlapping online windows.
Failure scenarios
| Scenario | Outcome |
|---|---|
| Relay is down when sender stores | Sender retries or waits for Hyperswarm sync |
| Relay is down when recipient fetches | Recipient retries later or waits for Hyperswarm sync |
| Blob expires before fetch (30-day TTL) | Data deleted from relay; Hyperswarm sync handles delivery |
| Relay loses data (crash, disk failure) | No data loss — Hypercore feeds are the source of truth |
| Relay compromised | Attacker sees encrypted blobs but cannot decrypt them |
Relay discovery
Current: manual configuration
Relays are configured explicitly by the user:
- Via AI conversation: "Add relay at https://relay.example.com"
- Stored in the
relay_configdatabase table withrelay_type = 'peer'
Each relay entry includes:
| Field | Description |
|---|---|
relay_url | Full URL of the relay endpoint |
relay_type | 'peer' for Hypercore relays, 'nostr' for message relays |
enabled | Whether the relay is active |
Default relay
A default relay service is planned but not yet deployed. Currently, users must configure their own relays — either a friend's gateway with relay mode enabled, or a self-hosted cloud gateway.
Future: automatic discovery via DHT
A planned enhancement will allow Crow to discover relays automatically through the Hyperswarm DHT:
- Relay operators announce their availability on a well-known DHT topic
- Crow clients query the topic to find available relays
- Clients select relays based on latency, capacity, and trust preferences
This is not yet implemented. Current relay discovery is manual only.
Capacity limits and retention
Per-relay defaults
| Parameter | Value | Configurable |
|---|---|---|
| Max blob size | 1 MB | Yes (relay operator) |
| Max pending blobs per recipient | 100 | Yes (relay operator) |
| Blob TTL | 30 days | Yes (relay operator) |
| Rate limit | Per contact per hour | Yes (relay operator) |
Quota enforcement
- Store requests that exceed the blob size limit receive
413 Payload Too Large - Store requests that exceed the per-recipient blob count receive
429 Too Many Requests - Expired blobs are purged by a periodic cleanup routine (
cleanupExpiredBlobs())
Storage considerations
A relay with 100 contacts, each with 100 pending 1MB blobs, would use approximately 10 GB of storage. In practice, usage is much lower — most blobs are delivered within hours and deleted.
The current implementation uses an in-memory store (Map). A production relay should use persistent storage (SQLite or similar) to survive restarts.
Authentication model
All relay endpoints require Ed25519 signature authentication:
Store authentication
- Sender constructs
message = JSON.stringify({ recipient, blob }) - Sender signs
messagewith their Ed25519 private key - Relay verifies the signature against
senderPubkey - Relay checks that
senderPubkeybelongs to a known contact
Fetch authentication
- Recipient constructs
message = "<pubkey>:<timestamp>" - Recipient signs
messagewith their Ed25519 private key - Relay verifies the signature against
pubkey - Relay checks that
timestampis within 5 minutes of server time (replay protection)
Trust model
Relays only serve authenticated contacts. An unknown public key cannot store or fetch blobs. This prevents:
- Spam — Anonymous parties cannot fill the relay with junk data
- Enumeration — Unauthenticated requests cannot discover which public keys have pending blobs
- Abuse — Rate limits are enforced per authenticated contact
Implementation
The relay is implemented in servers/sharing/relay.js and exposes two Express route handlers via createRelayHandlers():
store(req, res)— Validates, authenticates, and stores a blobfetch(req, res)— Authenticates and returns all pending blobs for the requester
The gateway mounts these at /relay/store and /relay/fetch when relay mode is enabled.
Module dependencies
servers/sharing/relay.js
└── servers/sharing/identity.js (verify function for Ed25519 signatures)Related modules
| Module | Role in relay system |
|---|---|
servers/sharing/relay.js | Store-and-forward logic, Express handlers |
servers/sharing/identity.js | Ed25519 key management, signature verification |
servers/sharing/peer-manager.js | Hyperswarm discovery (direct P2P, no relay) |
servers/sharing/sync.js | Hypercore replication (direct P2P, no relay) |
servers/sharing/server.js | MCP tools including crow_sharing_status (relay_enabled toggle) |