Skip to main content
HashDrop’s peer-to-peer file transfer is built on top of WebRTC data channels, managed through the PeerJS library. This page explains how ICE negotiation works, how ICE servers are configured and fetched, how files are streamed as binary chunks, and the transport policy that governs connection attempts.

PeerJS

PeerJS wraps the browser’s native RTCPeerConnection and RTCDataChannel APIs to provide a simpler connection model. It handles:
  • Signaling: exchanging SDP offers/answers between peers through the PeerJS cloud or a self-hosted peerjs-server
  • ICE negotiation: gathering and exchanging ICE candidates so peers can find a route to each other
  • Data channels: opening a reliable binary channel once the connection is established
HashDrop creates a Peer instance with a Warp Code-derived ID (e.g., sr-warp-cosmic-falcon) and connects to a remote peer using the receiver’s matching ID. The DataConnection object returned by PeerJS is stored in Zustand’s useWarpStore and used for all file chunk transmission.

ICE negotiation

ICE (Interactive Connectivity Establishment) is the protocol WebRTC uses to discover the best network path between two peers. HashDrop configures ICE servers in src/lib/webrtc-ice.ts.

Default ICE servers

const DEFAULT_ICE_SERVERS: RTCIceServer[] = [
  { urls: 'stun:stun.l.google.com:19302' },
  { urls: 'stun:stun1.l.google.com:19302' },
  { urls: 'stun:stun2.l.google.com:19302' },
  {
    urls: [
      'turn:openrelay.metered.ca:80?transport=udp',
      'turn:openrelay.metered.ca:80?transport=tcp',
      'turn:openrelay.metered.ca:443?transport=tcp',
      'turns:openrelay.metered.ca:443?transport=tcp',
    ],
    username: 'openrelayproject',
    credential: 'openrelayproject',
  },
]
STUN servers let each peer discover its public IP address and NAT-mapped port. This is sufficient for most consumer networks. TURN servers relay traffic through an intermediary when a direct path is blocked by a symmetric NAT or firewall — a necessary fallback for corporate or mobile networks.

Dynamic ICE server fetch with fallback

At runtime, getIceServers() attempts to load fresh credentials from the /api/webrtc/ice-servers endpoint (which can be backed by a Metered TURN subscription for production-grade relay capacity). If the API is unavailable or returns an error, the function falls back transparently to DEFAULT_ICE_SERVERS.
export async function getIceServers(): Promise<RTCIceServer[]> {
  if (cachedIceServers) {
    return cachedIceServers
  }

  if (!iceServersPromise) {
    iceServersPromise = fetch('/api/webrtc/ice-servers', {
      method: 'GET',
      cache: 'no-store',
    })
      .then(async (response) => {
        if (!response.ok) {
          throw new Error(`ICE server request failed with ${response.status}`)
        }

        const payload = await response.json() as { iceServers?: RTCIceServer[] }
        if (!Array.isArray(payload.iceServers) || payload.iceServers.length === 0) {
          throw new Error('ICE server response was empty')
        }

        cachedIceServers = payload.iceServers
        return payload.iceServers
      })
      .catch((error) => {
        console.warn('[WebRTC] Falling back to default ICE servers', error)
        cachedIceServers = DEFAULT_ICE_SERVERS
        return DEFAULT_ICE_SERVERS
      })
      .finally(() => {
        iceServersPromise = null
      })
  }

  return iceServersPromise
}
Results are cached in memory for the lifetime of the page so that repeated connection attempts do not produce redundant API calls.

Data channel and chunked transfer

Once the WebRTC peer connection is established, PeerJS opens a reliable binary data channel. HashDrop splits the file into 16 KB chunks (ArrayBuffer slices), assigns each chunk a sequence number, and sends them in order. The receiver buffers incoming chunks and reconstructs the file once all chunks have arrived. Key properties of the data channel transfer:
  • Chunk size: 16,384 bytes (16 KB)
  • Transfer type: binary (ArrayBuffer)
  • Sequencing: each chunk carries an index; the receiver tracks received indices and rejects duplicates
  • Integrity: the sender computes a SHA-256 hash before sending; the receiver recomputes and compares after reassembly

Transport policy

export function getIceTransportPolicy(): RTCIceTransportPolicy {
  return 'all'
}
The transport policy 'all' tells WebRTC to try every candidate type — host (LAN), server-reflexive (STUN), and relayed (TURN) — in priority order. This maximises the chance of a direct connection while keeping relay as an automatic fallback.
When no direct or STUN-assisted path can be established, WebRTC falls back to the TURN relay servers included in the ICE server list. If relay via TURN is also unavailable, HashDrop can fall back to its own server-side relay mode. See Relay mode for details.
DTLS (Datagram Transport Layer Security) and SRTP (Secure Real-time Transport Protocol) encryption are mandatory in all WebRTC implementations. Every byte transmitted over a HashDrop data channel is encrypted end-to-end with no additional configuration required.

Build docs developers (and LLMs) love