http-relay
An HTTP relay for reliable asynchronous message passing between producers and consumers, with store-and-forward semantics and explicit acknowledgment.
Built primarily for Pubky applications, but usable as a general-purpose relay.
What is this?
An HTTP relay enables decoupled communication between distributed services. Producers POST messages to a channel; consumers GET them. The relay handles the coordination, storage, and delivery confirmation.
The problem it solves: When a mobile app requests data and then gets backgrounded or killed by the OS, the HTTP response never arrives—but from the server's perspective, it was sent successfully. This relay ensures messages persist until the consumer explicitly acknowledges receipt.
Use cases:
- Mobile apps that need reliable message delivery despite OS backgrounding
- Services that can't communicate directly (NAT traversal, firewall bypass)
- Decoupled microservices with delivery confirmation requirements
Features
- Store-and-forward - Messages persist until explicitly acknowledged
- At-least-once delivery - Consumers can retry; message stays available until ACKed
- Delivery confirmation - Producers can block until consumer ACKs, or check status
- Mobile-friendly timeouts - 25s default stays under typical proxy limits (nginx, Cloudflare)
- Content-Type preservation - Forwards producer's Content-Type to consumer
- Legacy compatibility -
/link/{id}endpoint for existing integrations. See old codebase
Installation
Or add as a dependency:
[]
= "0.6"
Usage
As CLI
# Default: bind to 127.0.0.1:8080 (localhost only)
# Bind to all interfaces (for production/Docker)
# Custom configuration
Options:
| Flag | Description | Default |
|---|---|---|
--bind <ADDR> |
Bind address | 127.0.0.1 |
--port <PORT> |
HTTP port (0 = random) | 8080 |
--inbox-cache-ttl <SECS> |
Message TTL for inbox | 300 |
--inbox-timeout <SECS> |
Inbox long-poll timeout | 25 |
--max-body-size <BYTES> |
Max request body size | 2048 (2KB) |
--max-entries <N> |
Max entries in waiting list | 10000 |
--persist-db <PATH> |
SQLite database path for persistence | (in-memory) |
-v |
Verbosity (repeat for more) | warn |
-q, --quiet |
Silence output | - |
As Library
use HttpRelayBuilder;
async
API
Inbox Endpoints
The primary API. Store-and-forward with explicit acknowledgment.
| Method | Endpoint | Description |
|---|---|---|
POST |
/inbox/{id} |
Store message (returns 200 immediately) |
GET |
/inbox/{id} |
Retrieve message (long-poll, waits up to 25s) |
DELETE |
/inbox/{id} |
ACK - confirms delivery |
GET |
/inbox/{id}/ack |
Returns true or false (was message ACKed?) |
GET |
/inbox/{id}/await |
Block until ACKed (25s default timeout) |
Inbox IDs act as shared secrets. Anyone who knows an ID can read/write/ACK that inbox. IDs should be cryptographically random (e.g., 128-bit UUIDs). Predictable IDs allow attackers to intercept or acknowledge messages. Messages persist in plaintext memory for the TTL duration. Do not relay sensitive one-time credentials unless encryption is applied at the application layer.
POST /inbox/{id} - Store Message
Producer stores a message. Returns immediately without waiting for a consumer. New value overrides the old value if it already exists.
Responses:
200 OK- Message stored successfully503 Service Unavailable- Server at capacity
GET /inbox/{id} - Retrieve Message (Long-Poll)
Consumer retrieves the stored message. If no message is available, waits up to 25 seconds (configurable) for one to arrive.
Responses:
200 OK- Returns message with original Content-Type408 Request Timeout- No message arrived within timeout (25s default)
DELETE /inbox/{id} - Acknowledge Delivery
Consumer acknowledges successful receipt. Clears the message from storage.
Responses:
200 OK- Message acknowledged and cleared
GET /inbox/{id}/ack - Check ACK Status
Producer checks if message was acknowledged.
Responses:
200 OK- Body containstrue(ACKed) orfalse(pending)404 Not Found- No message exists (not posted yet, or expired)
GET /inbox/{id}/await - Wait for ACK
Producer blocks until consumer acknowledges the message.
Responses:
200 OK- Consumer ACKed the message408 Request Timeout- No ACK received within timeout (default 25s)
Typical Flow
sequenceDiagram
participant P as Producer
participant R as Relay
participant C as Consumer
P->>R: POST /inbox/{id}
activate R
Note right of R: Store in SQLite<br/>acked = false
R-->>P: 200 OK
deactivate R
P->>R: GET /inbox/{id}/await
activate R
Note right of R: Subscribe to ACK<br/>(blocks up to 25s)
C->>R: GET /inbox/{id}
R-->>C: 200 OK + message
C->>R: DELETE /inbox/{id}
activate R
Note right of R: Set acked = true<br/>Clear message body<br/>Notify ACK waiters
R-->>C: 200 OK
deactivate R
R-->>P: 200 OK (ACKed)
deactivate R
Step by step:
- Producer POSTs message to
/inbox/{id}— returns 200 immediately - Producer calls GET
/inbox/{id}/await— blocks waiting for ACK - Consumer GETs message from
/inbox/{id}— receives payload (or waits up to 25s if not yet available) - Consumer DELETEs
/inbox/{id}— acknowledges receipt - Producer's
/awaitcall returns 200 — delivery confirmed
The consumer can call GET before the producer posts—it will long-poll up to 25s for the message to arrive. No polling loop needed.
Link Endpoint (Legacy)
Implements the standard HTTP Relay spec. Maintained for backwards compatibility but not recommended for new integrations.
| Method | Endpoint | Description |
|---|---|---|
POST |
/link/{id} |
Send message, block until consumer retrieves (10 min timeout) |
GET |
/link/{id} |
Retrieve message, block until producer sends (10 min timeout) |
Why prefer /inbox: The /link endpoint has no ACK mechanism—if the consumer
disconnects after receiving data, the producer still gets 200 OK. The 10-minute
timeout also exceeds typical proxy limits.
Client Implementation Patterns
Producer: Store and Wait for ACK
Consumer: Retrieve and ACK
Key Points
- Network errors are recoverable: Because messages persist until ACKed, both producer and consumer can safely retry on connection drops.
- Consumer must ACK: Call DELETE after processing. Until then, the message remains available (at-least-once delivery).
- Producer can verify delivery: Use /await to block until ACK, or /ack to check status without blocking.
- Message TTL: Unacknowledged messages expire after 5 minutes (configurable).
Limitations
TCP Cannot Detect Sudden Disconnects
When a consumer's network disappears suddenly (Wi-Fi off, tunnel, app killed), the relay cannot detect this immediately. TCP acknowledgments can take 30+ seconds to fail when packets vanish.
This is why /inbox uses explicit ACKs: the producer only knows delivery
succeeded when the consumer calls DELETE. If the consumer crashes before ACKing,
the message remains available for retry.
Development
# Run tests
# Run with debug logging
RUST_LOG=debug
Releasing
See RELEASING.md for how to publish a new version.