Skip to main content

Desktop (Electron)

A desktop app can do something a browser tab cannot: run a TURN server on the participant's machine and ask the router to forward a port to it. Freehop's Electron helper does both in the main process and hands the result to your page through an origin-restricted preload. Participants behind hard NATs or UDP-blocking networks can then reach this participant without any third party. The gateways concept page explains why.

Main process​

main.mjs
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');
});

installFreehopGateway(params)​

ParameterMeaning
ipcMainElectron's ipcMain. The helper registers five handlers on it.
allowedOriginsRequired array of exact HTTPS origins (HTTP only on loopback). Use the same origins in the preload argument. Only the main frame may invoke gateway IPC.
optionsPassed to the gateway (see below). Optional.
log(event, details) => void, for example to forward port-mapping results to your logs. Optional.

It returns { gateway, close() }. gateway stays null until a page first asks for it: the gateway starts on that first request, including for receive-only sessions. close() removes the IPC handlers, stops the TURN server and removes the router mappings (best effort).

Gateway options​

OptionDefaultMeaning
hostfirst non-internal IPv4 addressAddress the TURN server listens on
port3478Listener port, UDP and TCP
relayPortRange[49160, 49359]Ports for relay allocations, mapped on demand
portMappingtrueAsk the router for mappings (PCP, then NAT-PMP, then UPnP IGD). false disables it
externalAddressnoneWith portMapping: false: the public address of ports you forwarded by hand
credentialTtlSeconds7200Lifetime of per-peer credentials, bounded to 1–86400 seconds
relayScope'internal''internal' relays only between allocations on this gateway, so nobody can use it to reach other internet hosts. 'public' behaves like a general TURN server; use it only when every member is trusted
limitsbuilt-inTURN quotas; by default 6 allocations per username, 4 Mbit/s per allocation, 32 allocations and 20 Mbit/s per room, 40 Mbit/s in total

Preload: window.freehopGateway​

The preload (freehop/electron/preload) exposes window.freehopGateway only to pages whose origin appears in --freehop-origins=, a comma-separated list you pass through additionalArguments. Any other page in the window, such as a link a participant opens, never sees the gateway. The signing key stays in the main process, including for trusted pages.

MethodWhat it does
info()Starts the gateway if needed and resolves to its description, or null when this machine cannot be reached from outside
credentialsFor(tag, peer)Returns bounded credentials for a room granted to this renderer and current origin; never returns the signing key
allowRoom(tag)Lets the gateway serve a room tag
revokePeer(tag, peer)Revokes one peer's credentials and allocations in a room
revokeRoom(tag)Revokes a whole room

The main process validates the sending frame and its current origin, as well as every argument (room tags and peer ids must be base64url of the expected length) before it touches the gateway. A window's rooms are released on the gateway when its page navigates away, its origin changes or the window closes, so a page that never called leave() leaves nothing behind. The IPC channels are freehop:gateway-info, freehop:allow-room, freehop:credentials, freehop:revoke-peer and freehop:revoke-room.

In your page​

Nothing changes. connect() looks for window.freehopGateway, asks for the gateway's description, allows the ticket's room on it and includes it when it joins. From then on, every member of the room receives credentials minted for them, inside sealed envelopes.

const session = await connect(ticket, { media: { audio: true } });
// On the desktop, other participants can now reach this participant through its own gateway.

When info() resolves to null (no public address, no router that grants a mapping, or a router whose own external address is private), the app joins as an ordinary peer. Nothing fails; the other steps of the ladder stay available.

For kicks, session.update(newTicket, { dropped }) drops old links and revokes the entire previous room epoch on this participant's gateway, including unreported aliases. session.disconnectPeer(peerId) drops one local connection and its local gateway credentials; it does not revoke membership or stop another alias. session.kick() now throws with migration guidance. Use authority.kick() plus replacement tickets to revoke membership.

Reachability is assessed at startup. Automatic recovery after router mapping loss or address changes is not yet implemented.

Other Node.js programs​

Outside Electron, startGateway() from freehop/relay runs the same gateway. The application must revoke the old room with gateway.revokeRoom(oldTag) on key rotation when using a raw gateway without the Electron bridge. Node.js does not provide browser WebRTC APIs by itself; a media client needs a compatible WebRTC implementation. In such a privileged Node client, pass the gateway object itself as the gateway option of connect() or join(). Across a process boundary, send only public info() metadata plus a bounded credentialsFor(tag, peer) broker, using the Electron helper as the reference. Never send a signing key to a page. A session host that should offer its gateway without joining the call uses host sessions instead.

Next​

Host sessions turn the machine that hosts a session into the session's gateway.