Pair is a two-person, no-account P2P chat prototype. It uses WebRTC data channels for transport and Web Crypto ECDH + AES-GCM for an application-level encryption layer on top of WebRTC's DTLS encryption.
Install Node.js 20 or newer, then from this folder run:
npm install
npm startTo build the Linux package:
npm run distThe tarball and AppImage will be created in the dist folder. The AppImage is
the recommended Linux format: Pair can replace it and restart itself after an
update. A tarball installation is also updated in place when its folder is
writable. On its first graphical launch, Pair adds itself to your Linux
Applications menu; open it from there thereafter with no terminal command.
Run these commands from a Windows machine with Node.js and Visual Studio Build Tools installed. The first command recompiles the optional WASAPI capture addon against Pair's current Electron version; the installer still works with its JavaScript audio fallback if that addon is unavailable.
npm install
npm run rebuild:addon
npm run dist:winThe installer is created as dist\Pair Setup <version>.exe. To make a matched
Windows + Linux release and update manifest, run npm run dist:all followed by
npm run publish. Upload the Windows installer, Linux tarball, and Linux
AppImage to the matching GitHub release before committing/pushing
public/latest.json.
Every packaged build checks public/latest.json as it opens. If the manifest
has a newer version, Pair downloads the appropriate package, verifies its
SHA-256 checksum, installs it, and restarts automatically.
The browser version remains available for testing. Serve this folder over localhost or HTTPS; Web Crypto and WebRTC are restricted in insecure contexts in many browsers.
py -m http.server 5173Open http://localhost:5173 in two browser windows. For a real friend-to-friend connection, the prototype uses manual offer/answer exchange and public STUN servers, so it works when the peers can establish a direct route but may fail across restrictive NATs. Set PAIR_TURN in the desktop app to use your own TURN relay; it only relays already-encrypted bytes.
- Person A clicks Create invite and sends the pairing code to Person B.
- Person B pastes it, clicks Create reply, and sends the generated code back.
- Person A pastes the reply and clicks Apply reply.
This uses no Pair server. It can connect directly when the two networks permit WebRTC peer-to-peer traffic. Some NAT/firewall combinations cannot accept a direct connection; those require a TURN relay supplied by the people using it.
Pair no longer starts a signaling server automatically. Direct pairing above is the normal connection path. If you deliberately want room-code signaling for a network you control, run it manually:
npm run signalFor localhost testing use ws://localhost:8787. For a remote peer, put this server behind a TLS reverse proxy (or supply PAIR_TLS_KEY and PAIR_TLS_CERT paths) and use wss://YOUR_DOMAIN:8787. Both people must use the same room code of at least 16 characters. This service only forwards WebRTC setup messages and stores no chat or file data.
If Windows Firewall asks whether Node.js can accept connections, allow it on the intended network. If your ISP uses CGNAT, port forwarding will not work; you would need a public VPS or a VPN overlay.
WebRTC cannot always connect two peers on different home networks directly — symmetric NAT blocks the direct ICE candidates, and without a relay the connection silently hangs even though signaling succeeded. The fix is a self-hosted TURN relay running on the host's PC via Docker.
One-time setup:
- Forward these ports on your router to this PC's LAN IP (replace
YOUR_LAN_IPwith your actual LAN IP, e.g.YOUR_LAN_IP):- TCP
3481→ internal3478(port 3478 was already taken by another device on this router) - UDP
3481→ internal3478 - UDP
50100–50200→ internal50100–50200(the relay port range coturn uses; the 49152–49551 range is reserved by Windows)
- TCP
- Start Docker Desktop, then double-click
coturn\start-coturn.bat(or rundocker compose -f coturn\docker-compose.yml up -d). coturn auto-restarts across reboots while Docker is running, so TURN stays available whenever either peer opens the app. - Replace
YOUR_PUBLIC_IP,YOUR_LAN_IP, andCHANGE_THIS_TO_A_LONG_RANDOM_SECRETinturnserver.confbefore starting. Generate the password with a password manager oropenssl rand -hex 32. - Start Pair with the same relay credentials on both devices (the app deliberately has no baked-in TURN password):
PAIR_TURN='[{"urls":["turn:YOUR_HOST:3481?transport=udp","turn:YOUR_HOST:3481?transport=tcp"],"username":"pair","credential":"YOUR_SECRET"}]' npm startTo verify it's reachable from outside your network, run from any other machine:
docker logs pair-coturn # local: should show no errors and several "allocate" lines after a callTo rotate the credential later: edit coturn\turnserver.conf (user=pair:... line), restart coturn, then start Pair with a matching PAIR_TURN value on both devices. No rebuild is needed:
set PAIR_TURN=[{"urls":"turn:YOUR_HOST:3481","username":"pair","credential":"YOUR_SECRET"}]TURN only relays already-encrypted WebRTC bytes (DTLS-SRTP); it cannot read any chat, file, or voice content.
Files are sliced into chunks, encrypted independently, and streamed with a 128 MB in-flight window plus concurrent encrypt/decrypt work so a single direct wired peer can keep a fast SCTP link saturated. The whole file is never loaded into memory during sending. Receiving very large files requires a Chromium browser with the File System Access API (or the Pair app's disk streaming); otherwise the fallback collects chunks in memory and is suitable only for smaller files. The 200 GiB limit is enforced on both send and receive.
This is an MVP, not a production security audit. Before relying on it for sensitive data, add authenticated device identity/fingerprint verification, replay protection, a robust signaling UX, TURN support, and audited cryptographic protocol implementations.