Connection lifecycle
- You create a transfer and share the link or short code with the recipient.
- Both devices join the same room on the signaling server.
- The server assigns roles: the first peer to join is the sender, the second is the receiver.
- Peers exchange a WebRTC offer, answer, and ICE candidates through the server.
- Once a usable network path is found, a direct data channel opens between the two devices.
- The signaling server plays no further part. All file data flows over the data channel.
What the signaling server does
- Assigns roles: the first peer to join a room becomes the sender, the second becomes the receiver.
- Relays WebRTC negotiation messages: the initial offer, the answer, and ICE candidates.
- Issues short-lived TURN credentials when a TURN server is configured.
- Registers short human-readable codes (e.g.
olive-tiger-castle) that map to room IDs. - Adds the size of each completed transfer to one anonymous global byte total, for the public homepage counter.
What the signaling server never does
- Handle file data of any kind.
- Store files, file names, transfer content, or logs of what you send or receive.
- Link the byte total to you. The counter is a single shared number with no per-user or per-transfer records.
- Persist room or peer information after the session ends.
Technical details
Technical details
Transports: Browser clients connect via Socket.IO. CLI clients connect via WebSocket at
/ws. Both share a single in-memory rooms registry (Map<roomId, [peer, peer]>), so browser-to-CLI transfers are fully supported.WebRTC negotiation flow:- Sender joins the room (
join-roomevent) and receives thesenderrole. - When the receiver joins, the server emits
user-connectedto the sender. - Sender creates an SDP offer and sends it via the
signalevent. - Receiver sets the remote description, creates an SDP answer, and sends it back.
- Both peers exchange ICE candidates via
signalevents (trickle ICE). - Once ICE negotiation completes, the WebRTC data channel opens directly between the peers.
POST /api/code with a room UUID. The server picks three random words from a 288-word list using a cryptographically secure random source (crypto.randomInt, not Math.random), because the code phrase is the only secret guarding a transfer, and maps them to the room ID with a 10-minute TTL. GET /api/code/:code resolves a code back to its room UUID.TURN credentials: Issued via GET /api/turn-credentials. On floe.one the server mints short-lived credentials (up to 24 hours) from Cloudflare’s Realtime TURN service. Self-hosted instances can instead use coturn with HMAC-SHA1 credentials via TURN_SECRET, or skip TURN entirely, in which case the endpoint returns public STUN servers only.Rate limiting: 30 connections per IP per 60 seconds for Socket.IO and WebSocket; 20 requests per IP per 60 seconds for the TURN credential endpoint; 60 requests per IP per 60 seconds shared across POST /api/code and GET /api/code/:code; 60 reports per IP per 60 seconds for the stats endpoint. The signaling server also caps the number of simultaneously-live room codes (10000 by default); POST /api/code returns 503 when that cap is hit.Global stats counter: After a transfer completes, the receiver sends POST /api/stats/report with just the byte count. The sender never reports. The server keeps a single anonymous running total (cached in memory, optionally persisted to Upstash Redis) and serves it from GET /api/stats for the homepage counter. No file names, contents, or per-transfer records are stored. The total is viewable only in the browser; the CLI receiver contributes but never displays it. Both the browser receiver and CLI receiver can opt out (browser: “Contribute to global stats” toggle; CLI: --no-report flag or FLOE_NO_STATS=1). See the architecture reference for details.Data-channel transfer protocol: Once the WebRTC data channel opens, the sender runs this sequence for each file:- Sends a JSON
metadatamessage with the filename, size in bytes, and the file’s index and total count in the batch. - Waits for the receiver to reply with a JSON
ackmessage. The ack carries a byte offset, so a transfer can resume mid-file. - Streams the file as binary chunks until the whole file is sent. Both senders size chunks adaptively to what the connection negotiates, up to 256 KB, and apply backpressure-based flow control, pausing when the data channel’s send buffer fills.
- Sends a JSON
endmessage to signal completion.