IFM Desktop Apps
Platform-native desktop applications built on the IFM Rust core via Tauri. Zero-config, one-command install, peer-discoverable on local networks.
Overview
| App | Purpose | Target | Binary Name |
|---|---|---|---|
| IFM Station | Broadcaster studio | Content creators, radio hosts | ifm-station |
| IFM Relay Station | Mesh relay + operator dashboard | Node operators, infrastructure | ifm-relay-station |
| IFM Radio Listener | Consumer listening app | End users, audience | ifm-radio-listener |
| IFM Dashboard | Internal mesh observability (telemetry consumption only) | Developers, operators | ifm-dashboard |
All three apps (and the Dashboard):
- Wrap the Rust core (
ifm-corevia N-API/TAURI) — no custom protocol logic - Platform-agnostic — Windows, macOS, Linux (x86_64 + arm64)
- Zero-config — Open and run; auto-discovers peers on LAN
- Peer-discoverable — Stations/relays announce via mDNS + DHT; listeners find them automatically
- One-command install —
curl ... | bashper app
IFM Dashboard is different by design — it is an internal observability tool that consumes each node's read-only telemetry endpoint (localhost HTTP,
ifm-coreObservabilityServer). It never runs a node and never joins the mesh: it cannot affect the network. See docs/dashboard/README.md and docs/architecture/observability.md.
Shared UI direction — SDK examples follow the desktop apps, ALWAYS. Shared UI code (e.g. the broadcast console in
packages/ifm-station/src/mixer/) is authored HERE and synced INTO the SDK examples (packages/sdk/examples/*). The desktop apps are the source of truth; the examples mirror them and never diverge (Architectural Invariant 9). Sync is always desktop → example, never the contrary.
Architecture
Key Design Decisions
| Decision | Rationale |
|---|---|
| Tauri + Rust core | Native performance, small binary (~15 MB), system integration |
| WebView UI (React) | Shared code with web apps, familiar stack, hot reload in dev |
| N-API for core | Direct Rust↔JS calls, no serialization overhead for hot paths |
| mDNS + DHT discovery | Works on LAN without internet; falls back to DHT for internet peers |
| System tray + background | Relay Station runs headless; Listener keeps audio on lock screen |
| Auto-update via GitHub Releases | Same pipeline as CLI; cosign-verified |
Shared Foundation: ifm-desktop-core
All three apps share a common crate at crates/desktop/:
crates/desktop/
├── Cargo.toml
├── src/
│ ├── lib.rs # Tauri commands, app lifecycle
│ ├── node.rs # IFM Node wrapper (tune, broadcast, scan)
│ ├── discovery.rs # mDNS announce + browse, DHT bootstrap
│ ├── audio.rs # Opus encode/decode, playback, capture
│ ├── tray.rs # System tray, notifications, background
│ ├── config.rs # Persistent config (identity, relays, freqs)
│ └── update.rs # GitHub Releases auto-updateExports (Tauri commands):
node_create(config)→NodeHandlenode_tune(handle, freq)→Resultnode_broadcast(handle, payload)→Resultnode_scan(handle)→Frequency[]node_peers(handle)→PeerInfo[]audio_start_capture(handle)→StreamHandleaudio_start_playback(handle, stream)→Resultdiscovery_start_mdns()/discovery_stop_mdns()tray_setup(menu)/tray_set_title(text)config_get(key)/config_set(key, value)update_check()/update_install()
Discovery & Peer Availability
Requirement: All running nodes discoverable to peers even on local computer.
Mechanism
mDNS (Bonjour/Avahi) — Announces
_ifm._tcp.localwith:- Node type:
station|relay|listener - Frequency (if station)
- Public key fingerprint
- WebSocket port (for local WebView connections)
- Capabilities:
audio,chat,files - mDNS dials prefer QUIC (UDP) addresses — an outbound TCP dial to a peer on the same machine fails with
EADDRINUSEon macOS, so the QUIC multiaddr is the reliable same-host dial path.
- Node type:
Kademlia DHT — Bootstrap via well-known peers; advertises:
- Multiaddrs (QUIC, WebSocket, WebRTC)
- Node metadata (same as mDNS)
- TTL: 10 min, refresh every 2 min
Same-machine mesh rendezvous (deterministic) — every desktop app also publishes its QUIC multiaddrs (plus TCP socket fallbacks) to a shared
~/Library/Application Support/ifm-mesh/<node_id>.json(TTL 30s, refreshed every 3s). Each app dials every fresh foreign entry viaNode::add_peer(the peer's libp2p PeerId is derived from its ed25519 verifying key), QUIC-first. This is what connects the apps on one computer even where macOS local-network privacy blocks mDNS for unsigned bundles; records then propagate through the normal Identify + reannounce-on-connect path.IFM_NO_MDNS=1disables mDNS dialing (hermetic tests / operators who bootstrap only).Local WebSocket bridge — Desktop apps expose
ws://127.0.0.1:<port>for:- WebView ↔ Core communication
- Other local apps (e.g., browser listener connecting to local station)
Audio notes (macOS)
Microphone permission — macOS WKWebView only exposes
navigator.mediaDevices.getUserMediawhen the app's Info.plist declaresNSMicrophoneUsageDescription. The Station bundle ships it viabundle.macOS.infoPlist→packages/ifm-station/src-tauri/Info.plist. Without it the mixer reports "microphone capture is not supported in this browser". (tauri-utils ≤2.9.x expects that key to be a path to a plist file, not an inline map.)Screen & System Audio Recording permission — the Station's Window and Desktop Audio sources enumerate/capture targets through
ScreenCaptureKit(SCShareableContent→SCContentFilter→SCStream). macOS requires the Screen & System Audio Recording TCC permission for that API; until it is granted, the console's source lists come back empty and show the "IFM needs permission" hint. Grant it from System Settings, then quit and reopen the app — TCC changes only apply after the process restarts (a Refresh button alone won't pick them up). Steps (from Apple's documentation):- Choose Apple menu → System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
- Click Screen & System Audio Recording.
- For each app listed, turn the ability to record on or off. You can allow apps to record both your screen and audio, or just your audio.
- To add an app to a list, click the Add button below the list, then navigate to the app you want to add.
The app declares
NSScreenCaptureUsageDescriptioninpackages/ifm-station/src-tauri/Info.plist(wired viabundle.macOS.infoPlist) so it is eligible to be added. Development wrinkle — macOS 15+ (Sequoia): the TCC permission is attached to the binary that callsScreenCaptureKit, and Sequoia only honors it for a stable code identity. A bare dev executable (target/debug/ifm-station) is ad-hoc/linker-signed and its signature changes on every recompile, so a Screen & System Audio Recording grant added for it in System Settings never actually applies —CGPreflightScreenCaptureAccess()keeps returning false no matter how often you enable it, refresh, or restart. This is not fixable from the app side. The reliable path for testing window/desktop capture is to run a real.appbundle:bun scripts/build.mjs --desktop ifm-station(release) orcargo tauri build --debuginsidepackages/ifm-station/src-tauri(debug, faster), then grantScreen & System Audio RecordingtoIFM Station.appand launch it. The console detects thetarget/debugcase and explains this in the source picker.Container-aware playback — the broadcaster's
MediaRecorderproducesaudio/webm;codecs=opuson Chrome but fMP4/AAC (audio/mp4) on macOS WKWebView. The Listener detects the container from the first chunk (EBML magic vsftypbox) and creates the matching SourceBuffer (audio/mp4;codecs=mp4a.40.2when supported), so live audio plays from either encoder.Relay integrity — TTL is excluded from the packet signing view, so relays can decrement and forward without re-signing; relayed copies verify and deduplicate as the same packet. (Previously the TTL was signed, every relayed copy failed verification, and multi-hop delivery was silently dropped.)
Listener Discovery Flow
Installation
One-Command Install (per app)
# IFM Station — Broadcaster studio
curl -fsSL https://ifm.sh/install-station.sh | bash
# IFM Relay Station — Mesh relay + dashboard
curl -fsSL https://ifm.sh/install-relay-station.sh | bash
# IFM Radio Listener — Consumer app
curl -fsSL https://ifm.sh/install-radio-listener.sh | bashPackage Managers
| Platform | Station | Relay Station | Radio Listener |
|---|---|---|---|
| Homebrew | brew install ifm-station | brew install ifm-relay-station | brew install ifm-radio-listener |
| winget | winget install IFM.Station | winget install IFM.RelayStation | winget install IFM.RadioListener |
| Scoop | scoop install ifm-station | scoop install ifm-relay-station | scoop install ifm-radio-listener |
| Chocolatey | choco install ifm-station | choco install ifm-relay-station | choco install ifm-radio-listener |
| apt | apt install ifm-station | apt install ifm-relay-station | apt install ifm-radio-listener |
| pacman | pacman -S ifm-station | pacman -S ifm-relay-station | pacman -S ifm-radio-listener |
| dnf | dnf install ifm-station | dnf install ifm-relay-station | dnf install ifm-radio-listener |
| apk | apk add ifm-station | apk add ifm-relay-station | apk add ifm-radio-listener |
Manual Download
All releases at: https://github.com/ifmprotocol/ifm/releases/latest
| Platform | Station | Relay Station | Radio Listener |
|---|---|---|---|
| macOS (arm64) | ifm-station-darwin-arm64.dmg | ifm-relay-station-darwin-arm64.dmg | ifm-radio-listener-darwin-arm64.dmg |
| macOS (x64) | ifm-station-darwin-x64.dmg | ifm-relay-station-darwin-x64.dmg | ifm-radio-listener-darwin-x64.dmg |
| Windows (x64) | ifm-station-windows-x64.msi | ifm-relay-station-windows-x64.msi | ifm-radio-listener-windows-x64.msi |
| Windows (arm64) | ifm-station-windows-arm64.msi | ifm-relay-station-windows-arm64.msi | ifm-radio-listener-windows-arm64.msi |
| Linux (x64) | ifm-station-linux-x64.AppImage | ifm-relay-station-linux-x64.AppImage | ifm-radio-listener-linux-x64.AppImage |
| Linux (arm64) | ifm-station-linux-arm64.AppImage | ifm-relay-station-linux-arm64.AppImage | ifm-radio-listener-linux-arm64.AppImage |
| Linux (deb) | ifm-station_<ver>_amd64.deb | ifm-relay-station_<ver>_amd64.deb | ifm-radio-listener_<ver>_amd64.deb |
| Linux (rpm) | ifm-station-<ver>.x86_64.rpm | ifm-relay-station-<ver>.x86_64.rpm | ifm-radio-listener-<ver>.x86_64.rpm |
Build & Release
Source Location
packages/
├── ifm-station/ # Tauri app
├── ifm-relay-station/ # Tauri app
├── ifm-radio-listener/ # Tauri app
└── ifm-dashboard/ # Tauri app (internal observability — passive consumer)Each is a standalone Tauri project with:
src-tauri/— Rust backend (usescrates/desktop)src/— React frontend (shared components with web apps)tauri.conf.json— App config (identifier, permissions, updater)package.json— npm scripts
Build Commands
# Build all desktop apps
bun scripts/build.mjs --desktop
# Build specific app
cd packages/ifm-station && bun run tauri build
cd packages/ifm-relay-station && bun run tauri build
cd packages/ifm-radio-listener && bun run tauri buildRelease Artifacts (added to releases/v<version>/)
releases/v0.1.0/
├── desktop/
│ ├── ifm-station-darwin-arm64.dmg
│ ├── ifm-station-darwin-x64.dmg
│ ├── ifm-station-windows-x64.msi
│ ├── ifm-station-windows-arm64.msi
│ ├── ifm-station-linux-x64.AppImage
│ ├── ifm-station-linux-arm64.AppImage
│ ├── ifm-station_0.1.0_amd64.deb
│ ├── ifm-station-0.1.0.x86_64.rpm
│ ├── ifm-relay-station-darwin-arm64.dmg
│ ├── ... (same matrix for relay-station)
│ ├── ifm-radio-listener-darwin-arm64.dmg
│ ├── ... (same matrix for radio-listener)
│ └── checksums-desktop.txtCross-App Integration
| Scenario | Mechanism |
|---|---|
| Browser listener → Local station | Station exposes ws://127.0.0.1:<port>; listener connects via mDNS |
| Local relay ↔ Internet peers | Relay announces on DHT; internet stations/Listeners connect via QUIC |
| Station → Multiple relays | Station selects relays from pool (mDNS + DHT) |
| Background relay | Relay Station runs in tray; ifm-relay-station --headless for servers |
Security
- Code signing — macOS (Developer ID), Windows (EV cert), Linux (cosign)
- Hardened runtime — Tauri CSP, no
eval, restricted filesystem access - Auto-update verification — cosign signatures on GitHub Releases
- Identity — Ed25519 keys stored in OS keychain (Keychain, Credential Manager, Secret Service)
Next: Individual App Documentation
- IFM Station — Broadcaster studio
- IFM Relay Station — Mesh relay + dashboard
- IFM Radio Listener — Consumer listening app