A technical explanation of the peer-to-peer file transfer process
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:
When the sender selects a file, the app generates a random UUID v4 (Universally Unique Identifier), for example:
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 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.
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.
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).
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.
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.
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.
Once ICE negotiation completes, WebRTC opens an RTCDataChannel between the two browsers. This channel is:
Before the file starts streaming, the app performs a brief application-level handshake:
file-info message (file name, size, MIME type, total chunks)ready messageTransferring 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.
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.
Before sending each chunk, the app checks the channel’s bufferedAmount property. If the buffer holds more than 8 MB of unsent data, the app pauses and waits until it drains below 4 MB before continuing. This prevents memory issues when transferring files on slow connections.
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.
As the receiver gets each chunk, it stores it in memory as an ArrayBuffer. When the sender signals done, the receiver assembles all chunks into a Blob with the correct MIME type, creates a temporary object URL, and triggers an automatic download.
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.
Both the sender and receiver listen for the connection’s close event. When a drop is detected during an active transfer, both sides immediately enter reconnect mode.
A 30-second countdown begins. During this window:
When the receiver reconnects, it sends a resume message telling the sender exactly how many chunks it has already received. The sender then resumes streaming from that chunk index, skipping all data that was already delivered successfully. No data needs to be re-transferred from the beginning.
{ type: "resume", chunksReceived: 1450 }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.
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.
Here is the full sequence from start to finish:
For more context about the project goals and privacy philosophy, see the about page.