Skip to main content
HashDrop establishes direct browser-to-browser connections using WebRTC. For peers behind NAT or firewalls to reach each other, WebRTC relies on ICE (Interactive Connectivity Establishment) — a process that tries STUN servers first to discover public IP addresses, then falls back to TURN servers to relay traffic when a direct path cannot be found. This page explains the default ICE configuration and how to replace it with your own TURN infrastructure for production use.

Default ICE servers

When no dynamic ICE server credentials are available, HashDrop falls back to a set of built-in servers defined in src/lib/webrtc-ice.ts:
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',
  },
]
Three Google STUN servers handle the common case where peers have routable public IPs. The openrelay.metered.ca TURN entry relays traffic for peers that cannot connect directly.
The openrelay.metered.ca TURN servers are a public community resource maintained by Metered.ca. They have no SLA, shared bandwidth limits, and publicly known credentials. Do not use them in production — any high-traffic or business-critical deployment should run its own TURN server or use a paid managed TURN service.

How ICE servers are resolved at runtime

The getIceServers() function in src/lib/webrtc-ice.ts is the entry point used by all WebRTC peer connections in HashDrop. It fetches credentials from the /api/webrtc/ice-servers endpoint and caches the result for the lifetime of the page:
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
}
If the API request fails for any reason — network error, missing credentials, or a non-OK response — the function logs a warning and returns DEFAULT_ICE_SERVERS automatically. This means the application stays functional even if the ICE endpoint is misconfigured, at the cost of falling back to the public relay servers.

Overriding ICE servers for production

The /api/webrtc/ice-servers route supports dynamic TURN credentials via Metered.ca. When the METERED_DOMAIN and METERED_SECRET_KEY environment variables are set, the route fetches short-lived credentials from the Metered API and returns them to the client instead of the public fallback servers. Add the following to your .env.local:
.env.local
METERED_DOMAIN=your-app.metered.live
METERED_SECRET_KEY=your_metered_secret_key
The credentials returned by Metered expire after one hour. Each new page load fetches a fresh set, so users are never stuck with stale credentials. If you prefer to use a different TURN provider or your own self-hosted server, replace the logic in src/app/api/webrtc/ice-servers/route.ts with a call to your provider’s credential API, and return the same { iceServers: RTCIceServer[] } JSON shape that the client expects.
For self-hosted TURN, Coturn is a widely used open-source server that runs on any Linux host. A minimal production setup needs a public IP, ports 3478 (STUN/TURN over UDP and TCP), and 5349 (TURNS over TLS) open in your firewall, and a TLS certificate. Coturn’s --use-auth-secret mode lets you generate short-lived credentials server-side without storing per-user passwords.

Build docs developers (and LLMs) love