Privacy-preserving push notification backend for the Mostro P2P trading ecosystem.
The server observes Nostr Gift Wrap events (kind 1059), looks up registered device tokens by trade_pubkey, and dispatches silent push notifications via Firebase Cloud Messaging (FCM) and UnifiedPush so Mostro Mobile clients can wake up and process trade events. Inspired by MIP-05.
┌──────────────────┐
┌─────────────────┐ 1. POST /api/register │ │
│ Mostro Mobile │ ──────────────────────────────────▶│ Push Server │
│ │ trade_pubkey + device_token │ │
│ │ │ Stores: │
│ Mostro Mobile │ 1b. POST /api/notify │ trade_pubkey → │
│ (sender) │ ──────────────────────────────────▶│ device_token │
│ │ trade_pubkey │ │
└─────────────────┘ └────────┬─────────┘
│
┌─────────────────┐ 2. Publishes kind 1059 ┌────────▼─────────┐
│ Mostro Daemon │ ──────────────────────────────────▶│ Nostr Relay │
│ / dispute │ p: trade_pubkey │ │
│ admin / peer │ └────────┬─────────┘
└─────────────────┘ │
┌────────▼─────────┐
│ Push Server │
│ observes event │
│ looks up token │
└────────┬─────────┘
│
┌────────▼─────────┐
│ FCM / UnifiedPush│
└────────┬─────────┘
│
┌────────▼─────────┐
│ Mostro Mobile │
│ wakes, fetches │
│ events │
└──────────────────┘
Two ingress paths feed the same dispatcher:
- Listener path — the Nostr listener subscribes to
kind 1059on configured relays and dispatches when aptag matches a registeredtrade_pubkey. - Sender-triggered path —
POST /api/notifylets a sender ask the server to wake the recipient when an event was sent peer-to-peer without going through the Mostro daemon (e.g. dispute admin DMs).
- The server stores
trade_pubkey -> device_tokenin memory only. No persistence other than UnifiedPush endpoint URLs. - The server does not authenticate
/api/register,/api/unregister, or/api/notify. Adding signatures or sender identifiers would let the operator correlate sender and recipient. /api/notifyalways returns202on parse-valid input. Registered and unregistered pubkeys are indistinguishable in status, body, and headers; rate-limit responses are byte-identical between the per-IP and per-pubkey paths. The endpoint cannot be used as an enumeration oracle.- Inbound
X-Request-Idon/api/notifyis stripped; the server generates its own UUIDv4 per request. - All
trade_pubkeys in logs go through a salted truncated BLAKE3 keyed hash (log_pubkey), with a per-process random salt that is never persisted. - The Nostr listener does not filter by
authors. Gift Wrap uses an ephemeral outer key, and admin DMs in disputes are user-to-user — an author filter would silently drop them.
What the server does see: an in-memory mapping of trade_pubkey -> device_token, and timing of incoming Gift Wrap events. It does not see message content, sender identity, or peer relationships.
- Rust 1.75 or later
- Access to one or more Nostr relays
- Optional: Firebase project with a service-account JSON for FCM
git clone https://github.com/MostroP2P/mostro-push-server.git
cd mostro-push-server
cp .env.example .env
# edit .env: NOSTR_RELAYS is required; FIREBASE_* if FCM is enabled
cargo run --releaseVerify it is up:
curl http://localhost:8080/api/health| Method | Path | Purpose |
|---|---|---|
| GET | /api/health |
Liveness |
| GET | /api/info |
Server version and feature flags |
| GET | /api/status |
Server status with token counts |
| POST | /api/register |
Register a device token for a trade_pubkey |
| POST | /api/unregister |
Remove a registered token |
| POST | /api/notify |
Trigger a silent push to the device for a trade_pubkey |
See docs/api.md for full request and response shapes.
docker build -t mostro-push-backend .
docker-compose up -d
docker-compose logs -f- docs/architecture.md — components, data flow, concurrency, privacy invariants
- docs/api.md — full HTTP contract
- docs/configuration.md — environment variables
- docs/deployment.md — Fly.io, Docker, nginx, systemd
- docs/unifiedpush.md — UnifiedPush backend notes
- docs/verification/dispute-chat.md — end-to-end runbook for the listener path
cargo test # in-process integration tests live alongside source
cargo clippy
cargo fmtA shell-script smoke test for a running instance:
RUST_LOG=info cargo run
./test_server.sh # in another terminalMIT.