← 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 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: ready message
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

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.

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.

File reassembly

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.

🔄 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

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.

Reconnect window

A 30-second countdown begins. During this window:

Resume from checkpoint

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.

Connection drops at chunk 1,450 of 2,000
Receiver reconnects and sends: { type: "resume", chunksReceived: 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 the UUID with the receiver (by any means).
  4. Receiver enters the UUID; 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 assembles all chunks into a Blob and triggers an automatic download.
  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.