← Back to File Transfer

How It Works

A technical explanation of the peer-to-peer file transfer process

📋 Overview

File Transfer uses WebRTC (Web Real-Time Communication) to establish a direct, encrypted data channel between two browsers. Once connected, the file is split into small binary chunks and streamed from the sender’s browser to the receiver’s browser in real time — without passing through any intermediate server.

The process has five distinct phases:

  1. Signaling — exchanging connection parameters via a relay server
  2. ICE negotiation — finding the best network path between both peers
  3. Opening the data channel — establishing the encrypted RTCDataChannel
  4. Chunked streaming — transferring the file in 64 KB binary chunks
  5. Reconnection & resume — recovering from dropped connections mid-transfer

🔑 Step 1 — The UUID Key & Signaling

When the sender selects a file, the app generates a random UUID v4 (Universally Unique Identifier), for example:

550e8400-e29b-41d4-a716-446655440000

This UUID becomes the sender’s peer ID in the PeerJS network. The sender’s browser registers this ID with a publicly accessible signaling server operated by PeerJS. The link the sender shares is simply the page address followed by ?receive= and this UUID.

The signaling server’s only job is to help the two browsers find each other and exchange WebRTC connection metadata (called SDP offers and answers). It never sees any file data.

What gets sent to the signaling server?

The signaling server only exchanges two kinds of messages:

Once both browsers have exchanged this information, the signaling server is no longer involved. All subsequent communication is peer-to-peer.

🖧 Step 2 — ICE Negotiation & NAT Traversal

Most devices sit behind a router using NAT (Network Address Translation), which means they don’t have a public IP address that another device can connect to directly. WebRTC solves this through a process called ICE (Interactive Connectivity Establishment).

STUN servers

Each browser contacts a STUN server (Session Traversal Utilities for NAT) to discover its own public IP address and port as seen from the outside internet. This public address is included in the ICE candidates sent to the other peer.

Candidate pairing

Both browsers collect all their possible network addresses (local network addresses, public NAT-mapped addresses) and try connecting to each of the other peer’s addresses in parallel. The first pair that succeeds becomes the active connection path.

TURN fallback

In some network configurations (strict firewalls, symmetric NAT) direct connections are not possible. In these cases, WebRTC falls back to a TURN server (Traversal Using Relays around NAT), which relays the data packets between peers. Even through a TURN relay, the data is still DTLS-encrypted and the relay server cannot read its contents.

Sender ↔ [STUN/TURN] ↔ Receiver
Handshake only — after ICE, data flows directly if possible

🔐 Step 3 — The Encrypted Data Channel

Once ICE negotiation completes, WebRTC opens an RTCDataChannel between the two browsers. This channel is:

Connection confirmation handshake

Before the file starts streaming, the app performs a brief application-level handshake:

Sender sends: file-info message (file name, size, MIME type, total chunks)
Receiver responds: start message (transfer ID, first chunk needed)
Sender begins streaming binary chunks

📦 Step 4 — Chunked File Streaming

Transferring a large file as a single binary message would overwhelm the browser’s send buffer and freeze the UI. Instead, the file is read and streamed in 64 KB chunks.

Why 64 KB?

WebRTC’s RTCDataChannel has a configurable send buffer. Sending chunks that are too large can cause the buffer to overflow, triggering errors or dropped connections. 64 KB is a conservative chunk size that keeps the buffer below saturation on typical networks while still providing good throughput.

Back-pressure control

Two limits keep memory usage flat. The sender never has more than 16 MB of chunks that the receiver has not yet acknowledged (a chunk is acknowledged once handed to the receiver’s disk writer, which buffers at most a few megabytes before writing), and it pauses whenever the data channel’s bufferedAmount exceeds 4 MB, resuming on the bufferedamountlow event. A slow network or a slow disk therefore simply slows the sender down instead of filling memory.

Progress tracking

Both sender and receiver track progress in terms of chunk count. The transfer percentage, current speed (in KB/s or MB/s), and estimated time remaining are updated in real time as each chunk is processed.

Writing to disk

The receiver never keeps the whole file in memory. Chunks are grouped into 4 MB blocks and written to disk as they arrive: in Chrome and Edge, directly into the file chosen in a save dialog shown before the transfer starts; in Firefox, through a service worker that streams them into the browser’s download manager. Browsers supporting neither (such as Safari) fall back to assembling the file in memory and downloading it at the end.

🔄 Step 5 — Reconnection & Resume

Network connections are not always reliable — a phone switching from Wi-Fi to cellular, a laptop going to sleep, or a brief router hiccup can drop the WebRTC connection mid-transfer. File Transfer handles this automatically.

Detection

A dropped connection does not always close cleanly: WebRTC can stay in a “disconnected” state indefinitely. Both sides therefore send a heartbeat every 5 seconds, and a connection that stays silent for 20 seconds is treated as lost, just like one that closes. The connection to the signaling server, needed to reconnect, is also re-established automatically if it drops.

Reconnect window

A 60-second countdown begins. During this window:

Resume from checkpoint

When the receiver reconnects, it sends a start message with its transfer ID and exactly how many chunks it has already received. The sender recognises the transfer, even if its own side had not yet noticed the drop, and resumes streaming from that chunk index. No data needs to be re-transferred from the beginning.

Connection drops at chunk 1,450 of 2,000
Receiver reconnects and sends: { type: "start", rxId: "…", from: 1450 }
Sender resumes from chunk 1,451 — no data is re-sent

🚫 What We Never See

Because all file data flows directly between browsers inside an encrypted channel, the following information is never accessible to us or to the PeerJS signaling server:

The only data that passes through PeerJS servers is the initial SDP and ICE handshake messages, which contain network addresses but no file data. These messages are ephemeral and not logged.

What about TURN relays?

In rare cases where a direct connection cannot be established, data may flow through a TURN relay server. TURN servers see encrypted packets but cannot decrypt them — the DTLS encryption is end-to-end between the two browsers, independent of the relay path.

📖 Summary

Here is the full sequence from start to finish:

  1. Sender selects a file; app generates a random UUID peer ID.
  2. Sender’s browser registers the UUID with the PeerJS signaling server.
  3. Sender shares a link containing the UUID with the receiver (by any means).
  4. Receiver opens the link and accepts the transfer; both browsers exchange SDP and ICE candidates via the signaling server.
  5. ICE negotiation finds the optimal network path (direct if possible, TURN-relayed if necessary).
  6. An encrypted RTCDataChannel is opened between the two browsers.
  7. Sender transmits file metadata; receiver confirms it is ready.
  8. File is read and streamed in 64 KB chunks over the data channel.
  9. Receiver writes the chunks to disk as they arrive (or assembles them in memory on browsers without disk streaming).
  10. If the connection drops, both sides attempt to reconnect and resume from the last received chunk.

For more context about the project goals and privacy philosophy, see the about page.