Components
| Component | Runtime | Port | Purpose |
|---|---|---|---|
| Client | Next.js 16 (React 19) | 3000 | Browser UI, WebRTC peer (via simple-peer) |
| Server | Node.js (Express 5, Socket.IO 4, ws 8) | 3001 | Signaling broker, TURN credential issuer |
| CLI | Go 1.25 (Pion WebRTC v4) | - | Headless sender/receiver |
| coturn | coturn (optional) | 3478, 5349 | TURN relay server |
Signaling transports
The server exposes two transports. Both share the samerooms registry (Map<roomId, [peer, peer]>) so browser-to-CLI transfers work transparently.
| Transport | Path | Used by |
|---|---|---|
| Socket.IO | /socket.io/ | Browser (web app) |
| WebSocket | /ws | CLI |
HTTP endpoints
| Method | Path | Description |
|---|---|---|
GET | / | Status check. Returns { "status": "ok", "timestamp": "..." }. |
GET | /health | Liveness probe. Returns { "status": "healthy", "uptime": <seconds> }. |
GET | /api/turn-credentials | Returns ICE server list. With TURN_SECRET set, includes coturn credentials. Without it, returns Google STUN servers only. Rate-limited to 20 requests per IP per 60 s. |
POST | /api/code | Registers a short code for a room ID. Body: { "roomId": "<uuid>" }. Response: { "code": "word-word-word" }. TTL: 10 minutes. |
GET | /api/code/:code | Resolves a short code to a room ID. Response: { "roomId": "<uuid>" }. Returns 404 if expired or not found. |
TURN credentials response
WhenTURN_SECRET and TURN_DOMAIN are set:
TURN_SECRET is unset:
username = "{expiry_unix}:floeuser", credential = base64(HMAC-SHA1(TURN_SECRET, username)). Expiry is 24 hours from issuance.
Socket.IO events (browser)
Client to server
| Event | Payload | Description |
|---|---|---|
join-room | roomId: string | Join a room. UUID format required. |
signal | { signal, target?, roomId? } | Forward a WebRTC signal (SDP or ICE candidate) to the other peer. |
ping | callback function | Keepalive. Server invokes the callback immediately. |
Server to client
| Event | Payload | Description |
|---|---|---|
room-joined | { role: "sender" | "receiver" } | Confirms room join and assigns role. |
user-connected | peerId: string | Notifies the sender that the receiver joined. |
signal | { signal, sender: peerId } | Delivers a WebRTC signal from the other peer. |
peer-disconnected | {} | The other peer left the room. |
room-full | {} | Room already has two peers. |
error | { message: string } | Server error. |
WebSocket messages (CLI, path /ws)
All messages are JSON objects with a type field.
Client to server
type | Other fields | Description |
|---|---|---|
join-room | roomId: string | Join a room. |
signal | signal, roomId: string | Forward a WebRTC signal. |
ping | - | Keepalive. Server responds with { "type": "pong" }. |
Server to client
type | Other fields | Description |
|---|---|---|
room-joined | role: "sender" | "receiver" | Role assignment. |
user-connected | id: string | Receiver joined. |
signal | signal, sender: string | WebRTC signal from the other peer. |
peer-disconnected | - | Other peer disconnected. |
room-full | - | Room already full. |
pong | - | Response to client ping. |
error | message: string | Server error. |
Rate limiting
| Limit | Value | Applies to |
|---|---|---|
| Connection rate | 30 per IP per 60 s (configurable via MAX_CONNECTIONS_PER_IP) | Socket.IO connections and WebSocket /ws connections, shared |
| TURN credential rate | 20 per IP per 60 s | GET /api/turn-credentials |
Map<ip, timestamp[]>) and cleaned every 60 seconds.
Signaling flow
- Sender calls
POST /api/codewith a UUID room ID to register a short code. - Sender joins the room via
join-room. Server assigns rolesender. - Sender prints the code and link and waits.
- Receiver joins the same room via
join-room. Server assigns rolereceiverand emitsuser-connectedto the sender. - Sender creates a WebRTC offer and sends it via
signal. - Receiver receives the offer, creates an answer, and sends it back via
signal. - Both peers exchange ICE candidates via
signal(trickle ICE). - WebRTC data channel opens. Signaling server plays no further part.
Data-channel transfer protocol
Once the WebRTC data channel is open, the sender runs the following sequence for each file. The CLI and browser use the same protocol and are fully interoperable.Message types
Metadata (JSON, sender to receiver)offset supports mid-file resume. In normal operation it is always 0.
Binary chunks (raw bytes)
The CLI sends 16 KB chunks. The browser sender sends larger adaptive chunks.
End marker (JSON, sender to receiver)
Backpressure
The sender pauses when the SCTP send buffer reaches the high-water mark (8 MB) and resumes when it drains below the low-water mark (4 MB). This prevents buffer overflow on large or slow transfers.Multi-file transfers
The four-step sequence (metadata, ack, binary chunks, end) repeats for each file in order. The receiver processes files sequentially.Room codes
Codes are three random words joined by hyphens (e.g.olive-tiger-castle), sampled from a 288-word corpus in server/words.json. On collision (extremely rare), a fourth word is appended. Codes expire 10 minutes after registration.