Skip to main content

Quickstart

The examples below show where each piece runs. A ticket-based integration uses a backend authority, a browser client and at least one gate. You can use public trackers instead of running your own gate. Desktop gateways and host nodes are optional; their owners contribute bandwidth when a direct path fails.

your backend your gate host every client hosts (optional)
──────────── ────────────── ──────────── ────────────────
createAuthority() ───► freehop-gate (gate) ◄─── connect(ticket) hostSession(ticket)
openRoom / ticket signalling only media peer to peer, desktop app or server
kick (rotate) never media or via a session gateway that hosts the session
│ ▲
└───── tickets over YOUR authenticated channel ─────┘ desktop apps: installFreehopGateway()

0. Install​

Alpha release on npm

Freehop alpha is published on npm. Install the current alpha with the command below. The backend, gate and host helpers need Node.js 22 or newer. The client runs in the browser; bundle its package import with your frontend build tool.

npm install freehop@alpha

All 15 public entry points include declarations. See the TypeScript guide for typed sessions, events and backend configuration.

The package has these entry points:

ImportUse it inWhat it gives you
freehop (also freehop/sdk)Browser or desktop rendererconnect(ticket, options)
freehop/authorityYour backend (Node.js)createAuthority()
freehop/ticketAnywhereencodeTicket(), decodeTicket(), validTicket()
freehop/hostThe machine that hosts a session (Node.js)hostSession(ticket)
freehop/electron and freehop/electron/preloadElectron main process and preloadinstallFreehopGateway() and window.freehopGateway
freehop/gateA gate host (Node.js)createGate(), also available as the freehop-gate command
freehop/clientBrowser, low leveljoin() with a raw secret instead of a ticket
freehop/relayNode.js or Electron mainstartGateway() and gateway lifecycle
freehop/memberNode.jsA gateway member that joins a room without media
freehop/turnNode.jsThe TURN server
freehop/port-mapperNode.js or Electron mainPCP, NAT-PMP and UPnP router mapping
freehop/stunNode.jsSTUN/TURN codec helpers
freehop/tokensBackend or gate (Node.js)Gate token signing and verification

1. Backend: decide who is in a room​

Your backend embeds an authority. It creates a secret for each room, and hands a ticket to each member your application admits. Only your own authentication decides who gets a ticket.

backend.mjs
import { createAuthority } from 'freehop/authority';

const authority = createAuthority({
app: 'my-app', // namespaces room keys
gates: ['wss://gate.example.com/freehop'], // one or more gates
gateTokenSecrets: {
'wss://gate.example.com/freehop': process.env.FREEHOP_GATE_TOKEN_SECRET
},
stun: ['stun:gate.example.com:3478'] // approved by your application
});

await authority.openRoom('room-42'); // creates the room secret
const ticket = await authority.ticket('room-42', 'user-7'); // only for members you admitted
// Send `ticket` to user-7 over your own authenticated channel (your API or websocket).

Use the same private gate signing key in the backend and gate environments. The authority sends each token only to its intended gate; clients use the approved stun list and ignore gate-supplied STUN hints.

A ticket carries the room secret, so it is as sensitive as room membership itself. Deliver it only to that member. Authority and tickets covers kicks and the ticket format.

2. Gate: run the signalling service​

A gate is a small WebSocket service that passes sealed envelopes. It never carries media, so it needs no TURN ports and no media bandwidth.

FREEHOP_GATE_PORT=8787 \
FREEHOP_GATE_PUBLIC_HOST=gate.example.com \
FREEHOP_GATE_STUN='0.0.0.0:3478,[::]:3478' \
FREEHOP_GATE_TOKEN_SECRET=… \
FREEHOP_GATE_TOKEN_AUDIENCE=wss://gate.example.com/freehop \
FREEHOP_GATE_TRUST_PROXY=1 \
./node_modules/.bin/freehop-gate

In a project that depends on freehop, ./node_modules/.bin/freehop-gate runs the installed service. Avoid an unpinned npx freehop-gate, which can resolve a different package from the registry. From a checkout, use node bin/freehop-gate.mjs. Put the gate behind your TLS proxy so clients reach it as wss://gate.example.com/freehop. Gate service has a systemd unit and a Caddy snippet.

No gate of your own

For experiments you can skip this step: a public WebTorrent tracker works as a gate. Replace the authority's gates list with ['bt+wss://tracker.openwebtorrent.com'] and remove gateTokenSecrets when no private gate remains. Keep an application-approved public STUN server in stun, such as stun:stun.cloudflare.com:3478, if you are not running your own. Trackers carry sealed envelopes but still see IP addresses and other connection metadata.

3. Client: join with the ticket​

This is browser JavaScript. Fetch ticket from your authenticated backend, then call connect() from a Join button on an HTTPS page (localhost works for development). addTile, mediaElementFor, showRoute and removeTile are functions you supply in your UI. The microphone, camera and leave calls below are separate examples for your controls; do not run them all immediately after joining.

To let Freehop adjust video per peer when it detects sustained congestion, opt in with adaptiveVideo: true; this remains off by default.

call.js
import { connect } from 'freehop';

const session = await connect(ticket, { media: { audio: true, video: false } });

session.on('peer', ({ id }) => addTile(id)); // an authenticated member appeared
session.on('track', ({ peer, track }) =>
session.attach(track, mediaElementFor(peer, track.kind))); // <audio> or <video> per peer
session.on('path', ({ peer, kind, via }) => showRoute(peer, kind, via));
session.on('peer-left', ({ id, reason }) => removeTile(id));

await session.setMicrophone(false); // mute without renegotiation
await session.setCamera(true); // asks for the camera on demand
const levels = await session.levels(); // { [peerId]: 0..1 } for speaking indicators
await session.leave();

Optional: a last resort before unreachable​

Both options below are off by default; without them nothing changes. Give tickets your own TURN servers, and let clients try predicted ports first:

backend.mjs
const authority = createAuthority({
app: 'my-app',
gates: ['wss://gate.example.com/freehop'],
// Your TURN provider's short-lived credentials, minted per ticket: [{ urls, username, credential }]
turn: async ({ roomId, member, expires }) => mintTurnCredentials(expires)
});
call.js
const session = await connect(ticket, { media: { audio: true }, portPrediction: true });
session.on('path', ({ kind, via }) => {
if (via === 'turn') showNote('Connected through your TURN relay');
});

The relay carries only calls that would otherwise be unreachable, still encrypted end to end, and its bandwidth is yours. A provider's free tier, or Freehop's own TURN server (freehop/turn) on a machine you already run, can keep that at zero. Port prediction needs at least two stun: servers and stays off by default until it is verified on real carrier networks. See connect() options and paths.

kind is one of direct, gateway, relay, bridged or unreachable once a route is known, and connecting before that. Show it to your users: it is honest about what happened. Report session.id to your backend so that it can tell the other members which peer to drop after a kick. See Client: connect for every option and event.

4. Desktop apps: open the participant's front door​

A desktop app can run a gateway on the participant's own machine: a TURN server made reachable through the participant's router (PCP, NAT-PMP or UPnP). Participants behind hard NATs or UDP-blocking networks can then reach that participant without any third party.

main.mjs (Electron main process)
import { app, BrowserWindow, ipcMain } from 'electron';
import { createRequire } from 'node:module';
import { installFreehopGateway } from 'freehop/electron';

const require = createRequire(import.meta.url);
const freehop = installFreehopGateway({ ipcMain, allowedOrigins: ['https://app.example.com'] });
app.on('will-quit', () => freehop.close());

app.whenReady().then(() => {
const win = new BrowserWindow({
webPreferences: {
preload: require.resolve('freehop/electron/preload'),
additionalArguments: ['--freehop-origins=https://app.example.com'],
contextIsolation: true
}
});
win.loadURL('https://app.example.com');
});

connect() finds window.freehopGateway automatically. The gateway starts when connect() first requests it, including for receive-only sessions. Desktop (Electron) explains the origin restriction and the options.

5. Hosts: let the session's own host relay for it​

Whoever hosts a session (your application server, a community server or a participant's desktop app) can make that machine the session's gateway. It joins the room without media and offers its gateway to the members.

host.mjs
import { hostSession } from 'freehop/host';

const host = await hostSession(await authority.ticket('room-42')); // a ticket without a member
if (!host.available) console.log('Not reachable from outside: no public address, no router mapping.');

// After a kick, follow the rotation and revoke the entire old room epoch:
await host.update(nextTicket, { dropped: [peerId] });
await host.close();
Budget for gateway traffic

A host node contributes upload when it relays media. If you run it on your own infrastructure, budget for that bandwidth and compute. A participant-owned host uses that participant’s connection. Gates remain signalling-only in either setup.

Run the reference example​

examples/minimal/ is a complete consumer in one process: a backend with an authority, a gate on the same origin, and a page with join, microphone and camera toggles, a path label and level meter per peer, and kicks through the app's own API.

git clone https://github.com/jolynstudios/freehop
cd freehop
npm ci --ignore-scripts
node examples/minimal/server.mjs # prints http://127.0.0.1:8790

Open the address in two or more browser windows and join the same room. test/browser/example-app.mjs drives the same app with three real browsers and asserts the mesh, the kick rotation and the removal.

To try the opt-in TURN last resort, start the example with your relay. Every ticket then carries it, but clients use it only when no other route works, so two windows on one machine still connect directly; a call it does carry reads "relay via turn":

FREEHOP_TURN_URLS=turn:turn.example.com:3478 FREEHOP_TURN_USERNAME=… FREEHOP_TURN_CREDENTIAL=… \
node examples/minimal/server.mjs

Next​

Read Gates to see what your server sees, or try a live call between two browser tabs.