Skip to main content
When you create a transfer in Floe, a unique room is created on the signaling server. The sender shares a code or link. When the recipient joins, the two devices introduce themselves through the server and exchange the information they need to connect directly. Once that handshake is complete, the server steps aside. The signaling server never touches file data, never stores your files or any record of what you send, and plays no part once the WebRTC connection is established. The only thing it keeps is a single anonymous running total of bytes transferred, used for the public counter on the homepage (see below).

Connection lifecycle

  1. You create a transfer and share the link or short code with the recipient.
  2. Both devices join the same room on the signaling server.
  3. The server assigns roles: the first peer to join is the sender, the second is the receiver.
  4. Peers exchange a WebRTC offer, answer, and ICE candidates through the server.
  5. Once a usable network path is found, a direct data channel opens between the two devices.
  6. 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.
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:
  1. Sender joins the room (join-room event) and receives the sender role.
  2. When the receiver joins, the server emits user-connected to the sender.
  3. Sender creates an SDP offer and sends it via the signal event.
  4. Receiver sets the remote description, creates an SDP answer, and sends it back.
  5. Both peers exchange ICE candidates via signal events (trickle ICE).
  6. Once ICE negotiation completes, the WebRTC data channel opens directly between the peers.
Short codes: Registered via 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:
  1. Sends a JSON metadata message with the filename, size in bytes, and the file’s index and total count in the batch.
  2. Waits for the receiver to reply with a JSON ack message. The ack carries a byte offset, so a transfer can resume mid-file.
  3. 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.
  4. Sends a JSON end message to signal completion.
For multi-file transfers, this four-step sequence repeats for each file in order.