Persistent P2P for the estate
Reusable WebRTC peer-discovery + connection layer. STUN NAT traversal · BroadcastChannel same-origin auto-discovery · manual offer/answer paste for cross-network. One connection layer every estate tool imports.
Node state
Own ID
—
Signal channel
fall-signal
Connected
0
Broadcasting
off
Sovereign. No signalling server. Public STUN only. All state lives in this browser. idle
Activity
Usage · one-liner
// import the library import { FallLink } from './falllink.js'; // instantiate with sensible defaults (Google + Cloudflare STUN, 'fall-signal' channel) const link = new FallLink({ ownId: 'my-node' }); // auto-discover peers on the same origin link.startBroadcast(); // listen for peers + messages link.on('peer', ({ peerId }) => console.log('peer up', peerId)); link.on('message', ({ peerId, data }) => console.log(peerId, data)); // broadcast to all connected peers link.broadcast({ type: 'hello', from: 'me' });
Connected peers
Every connection is a live WebRTC data channel. Latency measured via ping every three seconds.
Signal exchange
Two paths: same-origin via BroadcastChannel (automatic) or cross-network via copy/paste of offer + answer bundles.
Same-origin (auto)
Open this page in another tab on the same device. Peers find each other on the fall-signal BroadcastChannel automatically.
Cross-network · Alice creates an offer
Cross-network · Bob accepts an offer
ICE gathering. After creating a bundle, wait for ICE candidates to be gathered (button will say "Ready"). The bundle is base64-encoded JSON containing SDP + ICE.
Import into your tool
Drop-in ESM
import { FallLink } from 'https://sjgant80-hub.github.io/falllink/falllink.js'; // or vendor it into your repo: // curl -O https://sjgant80-hub.github.io/falllink/falllink.js const link = new FallLink({ ownId: crypto.randomUUID(), stunServers: [{ urls: 'stun:stun.l.google.com:19302' }], bootstrapPeers: [], signalChannel: 'fall-signal' });
API surface
// discovery link.startBroadcast() // same-origin auto-discovery via BroadcastChannel link.stopBroadcast() // manual signaling (cross-network paste) const { bundle, peerId, wrapper } = await link.createOffer(); const { bundle } = await link.acceptOffer(offerBundle); await link.acceptAnswer(answerBundle); // direct control const wrapper = await link.connect(peerId, offer?); link.broadcast(msg) // -> number of peers reached link.getPeers() // -> [{ peerId, state, latency, ... }] await link.ping(peerId) // -> latency ms link.destroy() // events link.on('peer', ({ peerId, wrapper }) => {}) link.on('message', ({ peerId, data }) => {}) link.on('disconnect', ({ peerId }) => {}) link.on('latency', ({ peerId, latency }) => {})