IFM Demos
Interactive demonstrations of the IFM protocol — three independent web apps for the three roles in the mesh, plus developer-facing products.
The Three Web Apps
Each app is a separate package in its own directory with its own package.json and bun dev. Run them together or standalone — every app boots the IFM web core (the Rust protocol core compiled to WASM) and joins the SAME mesh from the browser: the entry is always the rendezvous manifest JSON, exactly like production. To run against a LAN/local mesh instead, open any app with ?relay=ws://host:port.
The apps import the built SDK package (@ifm/sdk → packages/sdk/dist, linked live into each app's node_modules). After changing the SDK, rebuild it once — the demos pick up the fresh build on their next page load (bun install in an app refreshes the link if it ever goes stale):
cd packages/sdk && bun run build # rebuild dist after SDK changes| App | Role | URL | Directory |
|---|---|---|---|
| Station | Broadcaster / Origin | http://localhost:5173/ | packages/sdk/examples/station/ |
| Relay | Mesh Operator (passive observer dashboard) | http://localhost:5174/ | packages/sdk/examples/relay/ |
| Listener | End User | http://localhost:5175/ | packages/sdk/examples/listener/ |
The flow is always: Station broadcasts → Relay(s) forward → Listener hears, on the same frequency. Relays are station-agnostic infrastructure: a relay does not care who connects, it only relays every frequency and tracks everything running through it. Any station can use any relay, several stations can share one relay, and a station can use several — stations select their relays from the shared relay pool in the settings modal (or connect peers straight to the origin when none are selected). The Relay Station app is a passive observer dashboard: it watches the same mesh the desktop apps run on — stations on the discovery plane, connected peers, per-frequency traffic, and the shared relay pool (relays are brought up by their operators, never by the dashboard).
cd packages/sdk/examples/relay && bun install && bun dev # dashboard :5174
cd packages/sdk/examples/station && bun install && bun dev # broadcaster :5173
cd packages/sdk/examples/listener && bun install && bun dev # end user :5175Connecting Across Computers
Every app and the hub listen on all interfaces (0.0.0.0), so sessions on different machines can talk to each other — no code changes:
- Find the broadcasting machine's LAN IP:
ipconfig getifaddr en0(macOS). - Start the stack there (
bun scripts/start.mjsat the repo root) and allow the macOS firewall prompt. - On the other computer open
http://<ip>:5175/(listener) orhttp://<ip>:5173/(station). The apps auto-connect to the hub through the same-origin vite proxy, so the mesh is shared instantly.
To point a listener at a specific hub + station from anywhere, the listener (and station) accept deep-link parameters:
http://<ip>:5175/?relay=ws://<ip>:8790&freq=music.lofirelay— the hub to join:ws://host:port,host:port, or a bare host (defaults to port 8790).wss://is chosen automatically on HTTPS pages.freq— auto-tune the listener to a specific station frequency on load.
The listener also has a Connection card (sidebar) that shows the live relay status and the endpoint it is connected through, and copies a shareable listen link — just ?freq=…, since the connection is automatic: whoever opens it tunes in and the receiver resolves the closest hop itself — a close peer's hub first, then the nearest pool relay by round-trip time, then the station's origin directly, shown in the player (the ?relay= deep link above is only needed to point at a specific hub). The receiver is idle until you tune: it dials nothing on load — the Frequencies on air list is fetched from the directory hub over plain HTTP (GET /discover, no socket), so running public frequencies appear while the receiver stays disconnected. Tuning a frequency is what dials — the station's closest hop (a close peer's hub, the nearest pool relay, or the station's origin). ?freq= auto-tunes on load (a selected frequency, so dialing is correct).
Prefer the closest hop. A station is just a browser tab with no listening socket; audio + chat always flow through a hub — the closest one: the hub a close peer is on first, then the nearest pool relay, then the station's own hub. The hub also caches chat history and each frequency's voice init segment, so a listener that tunes in mid-stream can decode the audio and join a conversation already in progress.
Every frequency has a frequency number. The Station's setup card lets you enter a real FM number (e.g. 91.700) directly, or enter a name (e.g. music.lofi) and an available number is auto-assigned on creation. Names are optional labels on top of the number — listeners discover by name and tune by number.
Station — Broadcaster Studio
Purpose: Professional broadcasting interface for originating live content into the IFM mesh.
Location: packages/sdk/examples/station/
Features
- Real microphone streaming —
getUserMedia+MediaRecorder(Opus/WebM), streamed as livevoicepackets on the studio frequency - Studio setup — frequency Number/Name toggle (validated; a name auto-assigns a free frequency number — never reusing a number you already created), access type (public/protected with key), optional name
- Your stations — every station you open is saved locally (right of “Go on air.”) and reopenable/removable anytime; reopening a name keeps its number forever
- Reload-safe studio — a reload keeps you on the same page: the on-air studio is restored automatically (re-tunes + re-announces), and the setup form is remembered too
- Relay network view — the settings modal lists the live relay pool (station-agnostic, frequency-agnostic infrastructure — the shared pool the mesh gossips; relays are brought up by operators, never by apps). No relay is ever bound to a frequency or station: the station transmits through whichever pool relay the listener resolves as closest (a close peer first, then the nearest relay by round-trip time, then the station's origin directly)
- Live studio — on-air indicator, stop-audio control, real-time listener/peer view
- Chat — real-time text chat with everyone tuned to the frequency
- File transfer — send files to listeners on the frequency
Quick Start
cd packages/sdk/examples/station
bun install
bun dev
# Opens at http://localhost:5173/Architecture
Relay — Mesh Operator Dashboard
Purpose: A live operational dashboard for the IFM mesh (a passive observer — the relay pool itself is infrastructure run by operators).
Location: packages/sdk/examples/relay/
Features
- Web-core observer — the page boots the IFM web core as a passive observer node and joins the SAME mesh the desktop apps run on (entry = rendezvous manifest, exactly like production)
- Live Dashboard (React app, port 5174) — shows in real time:
- Stations on the discovery plane
- Connected peers
- Active frequencies with traffic
- The shared relay pool (read-only)
- Packets + bytes observed, uptime
- Live traffic feed (type, frequency, sender)
Quick Start
cd packages/sdk/examples/relay
bun install
bun dev
# Dashboard at http://localhost:5174/Architecture
Listener — End-User App
Purpose: Consumer-facing app for discovering, listening to, and chatting on IFM frequencies.
Location: packages/sdk/examples/listener/
Features
- Frequencies on air — live list of on-air stations, served by the directory hub over plain HTTP (no socket — the receiver stays idle until you tune); tune any of them, or type one
- Idle until tuned — nothing is dialed on load (no reconnecting spinner, no dead relay); tuning a frequency resolves the closest hop — a close peer's hub, then the nearest pool relay by round-trip time, then the station's origin directly — and it keeps running even when no relay or peer is in range; frequencies without a station are heard through the hub
- Protected frequency access — grant an access key (freq + key) so protected frequencies resolve their station and hop when you tune them; the key is hashed, never sent in clear
- Listen page — live Opus/WebM audio playback (MediaSource) with LIVE indicator, mute, "via peer / relay / origin" hop readout
- Chat — real-time text chat with the station
- Files — receive files sent by the station
Reliability details
- Mid-stream join — tuning a frequency that is already live works: the station re-broadcasts its voice init segment periodically, so a new listener's MediaSource can decode the live clusters that follow. The player then catches up to the live edge (a real “radio join” — you hear from now on, not from the station's start).
- Stop → go live — restarting the broadcast begins a brand-new WebM stream; the listener detects the new init segment and rebuilds its player, so audio keeps playing.
- Autoplay policy — real browsers block
audio.play()from non-gesture contexts. If sound is blocked, the app shows a “🔊 Sound blocked — click to enable” control and retries playback on any click/tap/keypress. - Connection resilience — the mesh is direct-first with relay assist: the app listens to the station (or a listener peer) directly, and a relay is dialed only when the direct signal degrades (measured in JS → the core's path scorer flips
best_pathto the relay). Browser↔browser traffic flows over the WebRTC overlay; browser↔native flows over WebSocket/WebTransport to the mesh entry.
Quick Start
cd packages/sdk/examples/listener
bun install
bun dev
# Opens at http://localhost:5175/Architecture
Developer-Facing Products (Moved Out of Demos)
These are not demos — they are shipping developer tools:
| Product | Type | Location | Purpose |
|---|---|---|---|
| Node UI | Terminal/TUI (Rust + ratatui) | crates/core/examples/node_ui.rs | Operator terminal for native node |
| CLI | Command-line (Rust, on ifm-core) | crates/cli/ | ifm for node management, frequency ops, identity, relay pool/binding |
| SDK | Library (Rust + WASM + TypeScript) | crates/sdk/, packages/sdk/ | Core library for building IFM applications |
Node UI
Terminal interface for node operators — mesh topology, frequencies, packets, health, logs.
cargo run --example node_ui -p ifm-coreCLI
# Node management
ifm node start --config /etc/ifm/node.toml
ifm node status
ifm node logs --follow
# Frequency operations
ifm freq list
ifm freq tune music.lofi
ifm freq broadcast --file track.opus
# Identity & keys
ifm identity generate
ifm identity export --pubkeySDK
# Rust
[dependencies]
ifm-sdk = { path = "../crates/sdk" }
# TypeScript (npm)
@ifm/sdk@latest
@ifm/opus-wasm@latestProject Structure (Web Apps)
packages/sdk/examples/
├── station/ # Broadcaster studio (port 5173)
│ ├── src/
│ │ ├── StationApp.tsx # Setup + Studio (mic, chat, files, peers)
│ │ ├── Header.tsx # Brand, relay status chip, cross-app links
│ │ └── style.css
│ ├── vite.config.ts
│ └── dev.mjs # vite only (the mesh joins from the browser)
├── relay/ # Mesh operator dashboard (port 5174)
│ ├── src/
│ │ ├── RelayApp.tsx # Live observer dashboard
│ │ └── style.css
│ ├── vite.config.ts
│ └── dev.mjs
└── listener/ # End-user app (port 5175)
├── src/
│ ├── ListenerApp.tsx # Station finder + player + chat + files
│ ├── Header.tsx
│ └── style.css
├── vite.config.ts
└── dev.mjsAll three apps import the one @ifm/sdk web core — there is no per-app copy of webCore.ts or the wasm glue anymore.
Contributing
The three web apps are independent products with separate repos/lifecycles planned. For now in monorepo:
- Station — Add features for broadcasters (multi-source, scheduling, monetization hooks)
- Relay — Add operational features (alerting rules, historical replay, capacity planning)
- Listener — Add consumer features (discovery algorithms, social, offline, accessibility)
When modifying core protocol (crates/core, crates/sdk):
- Update Station if it exposes new broadcaster capabilities
- Update Relay if it exposes new operator controls or metrics
- Update Listener if it enables new listener features
- Update CLI if it needs new management commands
- Update Node UI if it needs new tabs/data for operators