Skip to main content

Architecture

Freehop is a handful of small modules: a browser library, a gate, a gateway, and an SDK layer that connects them to your application. This page shows what runs where and follows one call from ticket to media.

The big picture​

sealed signalling only (kilobytes) media (megabytes), never through a gate

┌───────┐ ┌───────────┐ ┌───────┐ ┌───────┐ ─────────────── direct ──────────────── ┌───────┐
│ Ana │───│ gate(s) │───│ Ben │ │ Ana │ ─── a participant's own gateway (TURN) ───── │ Ben │
└───────┘ └───────────┘ └───────┘ └───────┘ ─── the session host's gateway ──────── └───────┘
any number of gates, run by anyone ─── forwarded by a participant ───────

On the left, what gates handle: sealed envelopes that introduce peers. On the right, media routes between machines that belong to the call. Your backend handles admission separately; an optional session gateway you host can also incur media bandwidth and compute costs.

What runs where​

MachineRunsHolds
Your backendThe authority (freehop/authority)Room secrets, in memory; your own notion of membership
Your gate hostThe gate service (freehop-gate)In-memory routing tables of room tags and peer ids. Nothing persistent.
Every browser or rendererThe client (connect())Its ticket, its derived room key, its links and media
A participant's desktop app (optional)The gateway, in the Electron main processThe signing key, kept in the main process; the page receives only bounded credentials
The session's host (optional)hostSession(): a gateway plus a gateway memberThe gateway secret; credentials for the members it serves

Module map​

ModuleSourceRuns inDependenciesJob
Clientsrc/client/Browser or rendererNoneRooms, sealed signalling, perfect negotiation, the path ladder, bridging and media
Tracker clientsrc/client/tracker-client.mjsBrowserNoneUses a public WebTorrent tracker as a gate (bt+wss://)
Gatesrc/gate/Node.js serverwsWebSocket rendezvous, optional STUN responder, token admission, byte budgets
TURN serversrc/relay/turn-server.mjsNode.js or Electron mainBuilt-insRFC 8656 subset: UDP relays for UDP, TCP and TLS clients, internal relaying between allocations
Port mappersrc/relay/port-mapper.mjsNode.js or Electron mainBuilt-insPCP, NAT-PMP and UPnP IGD client for router port mappings
Gatewaysrc/relay/agent.mjsNode.js or Electron mainBuilt-insTURN plus port mapping plus per-peer, per-room credentials
Gateway membersrc/relay/member.mjsNode.jsBuilt-ins, global WebSocketA host node that joins without media and offers its gateway
STUN codecsrc/shared/stun.mjsNode.jsBuilt-insSTUN and TURN messages (RFC 8489 and 8656), checked against the RFC 5769 vectors
Gate tokenssrc/shared/tokens.mjsNode.jsBuilt-insHMAC-SHA256 admission tokens
SDKsrc/sdk/Backend, browser, hostThe modules abovecreateAuthority(), connect(), hostSession(), tickets
Electron helpersrc/electron/ElectronThe gatewayGateway in the main process, origin-restricted preload

Around the library: bin/freehop-gate.mjs and deploy/ (the gate service), examples/minimal/ (a complete reference consumer), lab/ (the home-lab Linux NAT test harness) and test/ (unit tests and real-browser tests). The browser client has no runtime dependencies. The runtime is JavaScript, with TypeScript declarations for all 15 public package entry points. Desktop gateways advertise UDP and TCP TURN; TLS is an option on the standalone TURN server.

One call, step by step​

Ana and Ben join room-42. Ana is on a home router; Ben is on a phone hotspot.

  1. Tickets. Your backend opens the room (a 32-byte secret) and sends each of them a ticket over your own authenticated channel.
  2. Keys. Each client derives the room tag and the envelope key from the secret with HKDF. It picks a fresh random peer id.
  3. Gates. Each client connects to every gate in its ticket. On Freehop WebSocket gates: hello, then welcome (STUN hints are ignored), then join with the room tag and its peer id. The gate answers with the peers already present. Tracker gates use their own announce/offer/answer frames.
  4. Greeting. Every peer on a gate's roster is only a hint, so Ana greets Ben with sealed capabilities, and Ben greets Ana. When Ana's first envelope authenticates at Ben's side, Ben admits her as a member (peer event), and the other way round.
  5. Offer. The peer with the lower id opens the link and makes the offer; the other side waits for it. Descriptions and ICE candidates travel as sealed envelopes through every gate both are on.
  6. Endpoint phase. ICE gathers against application-approved STUN and any gateways named in the capabilities. Ana and Ben have 5 seconds (plus one grace period while checks get answers) to connect.
  7. Escalation, if needed. If they do not connect, the pair moves to the session phase (session gateways, 7 seconds), then bridging through a participant, then (only if the application opted in) port prediction and its own TURN relay, then unreachable with slow retries. See paths and escalation.
  8. Connected. The link's data channel opens, and from now on Ana and Ben send each other envelopes over it. Pair signalling uses the data channel. Gate keepalives, tracker discovery announcements and recovery traffic can still continue.
  9. Path. Each side reads its selected candidate pair and reports the path (direct, gateway, relay or bridged) to your interface.

Design choices​

  • Gates are untrusted and interchangeable. Because every envelope is sealed and bound to room, sender and recipient, a gate can be anyone's server, or a public tracker. Clients use several at once, and calls survive losing all of them.
  • Gateways speak plain TURN. A gateway is an ordinary TURN server with short-lived REST-style credentials, so every browser's ICE agent can use it without changes. The alternative, pinning Electron's WebRTC UDP port range and mapping it, would need cooperation from Chromium.
  • Escalation is earned. A pair only moves to a costlier route after the current attempt exhausts its time budget, and only one side restarts ICE, because simultaneous restarts were observed to silence Chromium's senders.
  • Full mesh, small rooms. Every participant sends its media to every other participant, with an initial target of 2 to 8 people. That target is not a capacity benchmark. At five participants with video, each one sends about 4 × 332 kbit/s upstream before overhead.
  • Membership lives in your backend. Freehop has no accounts and no lobby. Your application decides who gets a ticket, and a kick is a key rotation.

Next​

The protocol specification defines every message and timer, and the network test results show how it behaved in simulated networks in a home lab.