206 lines
8.3 KiB
Markdown
206 lines
8.3 KiB
Markdown
# UDP Nostr Relay — Architecture Plan
|
|
|
|
## Overview
|
|
|
|
A web page where a user signs in with their Nostr key, creates a message, and sends it as a single UDP datagram to a relay. The relay validates the signature, stores the event, and serves it to subscribers via the standard Nostr WebSocket protocol.
|
|
|
|
---
|
|
|
|
## Key Constraints Discovered
|
|
|
|
### 1. Browsers Cannot Send UDP
|
|
|
|
This is the critical constraint. Browsers have **no JavaScript API** for sending raw UDP datagrams to arbitrary hosts. The only UDP-like capability is WebRTC, which is designed for peer-to-peer media streams via ICE/STUN/TURN — not for firing a single datagram at a server.
|
|
|
|
**Implication:** The web page cannot send UDP directly. It must send the signed event to a **bridge** via HTTP or WebSocket, and the bridge fires the UDP datagram to the relay.
|
|
|
|
### 2. TCP and UDP Ports Are Separate Namespaces
|
|
|
|
A server can have **both** a TCP listener and a UDP listener on the **same port number** without conflict. This is how QUIC/HTTP3 works — it's UDP on port 443 alongside the HTTPS server on TCP port 443.
|
|
|
|
**Implication:** You can run a UDP Nostr receiver on port 443 even if your web server (nginx, caddy) is already using TCP port 443. They do not interfere.
|
|
|
|
### 3. The Relay Must Serve Subscribers via WebSocket
|
|
|
|
Nostr relays communicate with subscribers via WebSocket (NIP-01). The UDP receiver is just the **ingress** path. The relay still needs a WebSocket server for subscribers to connect and receive events.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
subgraph "User's Browser"
|
|
WP[Web Page<br/>Nostr sign-in + compose]
|
|
EXT[Nostr Extension<br/>nos2x / Alby / nsec]
|
|
end
|
|
subgraph "Relay Server"
|
|
BR[Bridge<br/>HTTP endpoint<br/>receives signed event]
|
|
UDPR[UDP Receiver<br/>port 443 or 8888<br/>listens for datagrams]
|
|
VAL[Signature Validator<br/>verifies event sig]
|
|
STORE[Event Store<br/>in-memory array]
|
|
WS[WebSocket Server<br/>port 8008<br/>Nostr relay protocol]
|
|
end
|
|
subgraph "Subscriber"
|
|
SUB[Nostr client<br/>connects via WebSocket]
|
|
end
|
|
|
|
WP -- "HTTP POST /publish" --> BR
|
|
WP -- "sign with nsec" --> EXT
|
|
BR -- "fire UDP datagram" --> UDPR
|
|
UDPR -- "raw event bytes" --> VAL
|
|
VAL -- "valid event" --> STORE
|
|
STORE -- "new event" --> WS
|
|
WS -- "NIP-01 EVENT message" --> SUB
|
|
|
|
style BR fill:#48a,color:#fff
|
|
style UDPR fill:#4a4,color:#fff
|
|
style WS fill:#a84,color:#fff
|
|
```
|
|
|
|
### Data Flow
|
|
|
|
1. **User composes message** in the web page, signs it with their Nostr key (via browser extension or in-page nsec)
|
|
2. **Web page POSTs** the signed JSON event to the bridge HTTP endpoint
|
|
3. **Bridge** fires the event as a single UDP datagram to the relay's UDP receiver (could be localhost or a remote address)
|
|
4. **UDP Receiver** receives the datagram, checks it fits in one packet (under 1472 bytes)
|
|
5. **Signature Validator** verifies the Schnorr signature against the pubkey in the event
|
|
6. **Event Store** appends the valid event to an in-memory array
|
|
7. **WebSocket Server** broadcasts the new event to all connected subscribers per NIP-01
|
|
|
|
---
|
|
|
|
## Component Specifications
|
|
|
|
### 1. Web Page (`udp_nostr_web/`)
|
|
|
|
**Files:**
|
|
- `index.html` — The page layout
|
|
- `app.js` — Nostr key management, event creation, signing, HTTP POST
|
|
- `style.css` — Minimal styling
|
|
|
|
**Capabilities:**
|
|
- Sign in with nsec (text input) or browser extension (nos2x/Alby via `window.nostr`)
|
|
- Compose a text message
|
|
- Create a Nostr event (kind 1) with the message as content
|
|
- Sign the event
|
|
- POST the signed JSON to the bridge at `/publish`
|
|
- Display the response (success/failure, event ID, size check)
|
|
|
|
**Constraints enforced client-side:**
|
|
- Warn if content exceeds ~1135 characters (would exceed 1472 byte UDP limit)
|
|
- Show the total JSON size before sending
|
|
|
|
### 2. Bridge + UDP Receiver + Relay (Single Node.js Process)
|
|
|
|
**File:** `udp_nostr_relay.js`
|
|
|
|
This is a single process that combines three roles:
|
|
|
|
#### A. HTTP Bridge (port 3000)
|
|
|
|
| Endpoint | Method | Body | Response |
|
|
|---|---|---|---|
|
|
| `/publish` | POST | Signed Nostr event JSON | `{ ok: true, id, size }` or `{ ok: false, error }` |
|
|
|
|
On receiving a valid event:
|
|
1. Validates the JSON structure (required fields present)
|
|
2. Fires the event as a UDP datagram to the UDP receiver (localhost:UDPPORT)
|
|
3. Sets the DF flag (Don't Fragment) to ensure single-packet delivery
|
|
4. Returns success/failure to the browser
|
|
|
|
#### B. UDP Receiver (port 443 or 8888)
|
|
|
|
- Listens for UDP datagrams
|
|
- Checks size ≤ 1472 bytes (single-packet guarantee)
|
|
- Parses JSON
|
|
- Validates required Nostr fields
|
|
- Verifies the Schnorr signature using the pubkey in the event
|
|
- If valid: stores the event and broadcasts to WebSocket subscribers
|
|
- If invalid: silently drops (no response — no handshake to probe)
|
|
|
|
#### C. WebSocket Server (port 8008)
|
|
|
|
- Implements the Nostr relay protocol (NIP-01 subset):
|
|
- `REQ` message: subscriber requests events (returns stored events + streams new ones)
|
|
- `EVENT` message: subscriber publishes an event (alternative to UDP path)
|
|
- `CLOSE` message: subscriber unsubscribes
|
|
- On new valid event (from UDP or WebSocket), broadcasts `EVENT` to all matching subscribers
|
|
|
|
### 3. Signature Verification
|
|
|
|
Uses the `@noble/secp256k1` library (pure JS, no native deps) to verify Schnorr signatures:
|
|
|
|
```
|
|
event_id = sha256(serialized_event_without_id_and_sig)
|
|
valid = schnorr_verify(event_id, sig, pubkey)
|
|
```
|
|
|
|
The event ID is computed from the serialized event fields `[0, pubkey, created_at, kind, tags, content]` per NIP-01.
|
|
|
|
---
|
|
|
|
## Port Strategy
|
|
|
|
| Port | Protocol | Service | Notes |
|
|
|---|---|---|---|
|
|
| 443 | **UDP** | Nostr UDP Receiver | Blends with QUIC/HTTP3 traffic. Does NOT conflict with TCP 443 (HTTPS). |
|
|
| 3000 | TCP | HTTP Bridge | Receives events from the web page. Could also be 443 if using a reverse proxy. |
|
|
| 8008 | TCP | WebSocket Relay | Standard Nostr relay port. Subscribers connect here. |
|
|
|
|
### Why Port 443 for UDP?
|
|
|
|
- **Censorship resistance:** UDP on port 443 is indistinguishable from QUIC/HTTP3 traffic to a passive observer. Blocking it would break a significant portion of the web.
|
|
- **No conflict:** TCP and UDP have separate port namespaces. Your HTTPS server (nginx on TCP 443) and the UDP Nostr receiver (UDP 443) coexist peacefully.
|
|
- **Practical consideration:** Some cloud providers and firewalls may block UDP on non-standard ports. Port 443 is universally allowed.
|
|
|
|
### Tradeoff
|
|
|
|
If you already have a service using **UDP on port 443** (e.g., a QUIC-enabled web server), you cannot have two UDP listeners on the same port. In that case, use a nearby port like 4443 or 8888.
|
|
|
|
---
|
|
|
|
## Implementation Plan
|
|
|
|
### Step 1: Create the Relay (`udp_nostr_relay.js`)
|
|
|
|
A single Node.js process with three components:
|
|
|
|
```
|
|
udp_nostr_relay/
|
|
├── relay.js # Main entry point — starts all three servers
|
|
├── package.json # Dependencies: @noble/secp256k1, ws
|
|
├── udp_receiver.js # UDP listener, signature validation, event storage
|
|
├── http_bridge.js # HTTP endpoint for browser POST
|
|
├── ws_relay.js # WebSocket Nostr relay for subscribers
|
|
└── event_store.js # In-memory event storage with subscription matching
|
|
```
|
|
|
|
### Step 2: Create the Web Page (`udp_nostr_web/`)
|
|
|
|
```
|
|
udp_nostr_web/
|
|
├── index.html # The page
|
|
├── app.js # Nostr key mgmt, signing, HTTP POST
|
|
└── style.css # Minimal styling
|
|
```
|
|
|
|
### Step 3: Test the Full Flow
|
|
|
|
1. Start the relay: `node relay.js`
|
|
2. Open the web page in a browser
|
|
3. Enter nsec or connect extension
|
|
4. Compose a message and send
|
|
5. Verify the event appears in the relay
|
|
6. Connect a Nostr client (e.g., `noscl` or another browser tab) via WebSocket to port 8008
|
|
7. Verify the subscriber receives the event
|
|
|
|
---
|
|
|
|
## Open Questions for You
|
|
|
|
1. **Port preference:** Do you want the UDP receiver on port 443 (blends with QUIC) or a simpler port like 8888 (no potential conflicts)?
|
|
2. **Nostr key handling:** Should the web page use a browser extension (nos2x/Alby) for signing, or accept an nsec directly? Extension is more secure but requires the user to have it installed.
|
|
3. **Relay location:** Is the relay on the same machine as the web page (localhost demo) or a remote server? This affects the bridge-to-UDP path.
|
|
4. **Persistence:** Should events persist across relay restarts (simple JSON file or SQLite) or is in-memory sufficient for the demo?
|