Skip to content

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

AppPurposeTargetBinary Name
IFM StationBroadcaster studioContent creators, radio hostsifm-station
IFM Relay StationMesh relay + operator dashboardNode operators, infrastructureifm-relay-station
IFM Radio ListenerConsumer listening appEnd users, audienceifm-radio-listener
IFM DashboardInternal mesh observability (telemetry consumption only)Developers, operatorsifm-dashboard

All three apps (and the Dashboard):

  • Wrap the Rust core (ifm-core via 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 installcurl ... | bash per 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-core ObservabilityServer). 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

DecisionRationale
Tauri + Rust coreNative performance, small binary (~15 MB), system integration
WebView UI (React)Shared code with web apps, familiar stack, hot reload in dev
N-API for coreDirect Rust↔JS calls, no serialization overhead for hot paths
mDNS + DHT discoveryWorks on LAN without internet; falls back to DHT for internet peers
System tray + backgroundRelay Station runs headless; Listener keeps audio on lock screen
Auto-update via GitHub ReleasesSame 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-update

Exports (Tauri commands):

  • node_create(config)NodeHandle
  • node_tune(handle, freq)Result
  • node_broadcast(handle, payload)Result
  • node_scan(handle)Frequency[]
  • node_peers(handle)PeerInfo[]
  • audio_start_capture(handle)StreamHandle
  • audio_start_playback(handle, stream)Result
  • discovery_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

  1. mDNS (Bonjour/Avahi) — Announces _ifm._tcp.local with:

    • 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 EADDRINUSE on macOS, so the QUIC multiaddr is the reliable same-host dial path.
  2. Kademlia DHT — Bootstrap via well-known peers; advertises:

    • Multiaddrs (QUIC, WebSocket, WebRTC)
    • Node metadata (same as mDNS)
    • TTL: 10 min, refresh every 2 min
  3. 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 via Node::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=1 disables mDNS dialing (hermetic tests / operators who bootstrap only).

  4. 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.getUserMedia when the app's Info.plist declares NSMicrophoneUsageDescription. The Station bundle ships it via bundle.macOS.infoPlistpackages/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 (SCShareableContentSCContentFilterSCStream). 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):

    1. Choose Apple menu → System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
    2. Click Screen & System Audio Recording.
    3. 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.
    4. 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 NSScreenCaptureUsageDescription in packages/ifm-station/src-tauri/Info.plist (wired via bundle.macOS.infoPlist) so it is eligible to be added. Development wrinkle — macOS 15+ (Sequoia): the TCC permission is attached to the binary that calls ScreenCaptureKit, 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 appliesCGPreflightScreenCaptureAccess() 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 .app bundle: bun scripts/build.mjs --desktop ifm-station (release) or cargo tauri build --debug inside packages/ifm-station/src-tauri (debug, faster), then grant Screen & System Audio Recording to IFM Station.app and launch it. The console detects the target/debug case and explains this in the source picker.

  • Container-aware playback — the broadcaster's MediaRecorder produces audio/webm;codecs=opus on Chrome but fMP4/AAC (audio/mp4) on macOS WKWebView. The Listener detects the container from the first chunk (EBML magic vs ftyp box) and creates the matching SourceBuffer (audio/mp4;codecs=mp4a.40.2 when 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)

bash
# 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 | bash

Package Managers

PlatformStationRelay StationRadio Listener
Homebrewbrew install ifm-stationbrew install ifm-relay-stationbrew install ifm-radio-listener
wingetwinget install IFM.Stationwinget install IFM.RelayStationwinget install IFM.RadioListener
Scoopscoop install ifm-stationscoop install ifm-relay-stationscoop install ifm-radio-listener
Chocolateychoco install ifm-stationchoco install ifm-relay-stationchoco install ifm-radio-listener
aptapt install ifm-stationapt install ifm-relay-stationapt install ifm-radio-listener
pacmanpacman -S ifm-stationpacman -S ifm-relay-stationpacman -S ifm-radio-listener
dnfdnf install ifm-stationdnf install ifm-relay-stationdnf install ifm-radio-listener
apkapk add ifm-stationapk add ifm-relay-stationapk add ifm-radio-listener

Manual Download

All releases at: https://github.com/ifmprotocol/ifm/releases/latest

PlatformStationRelay StationRadio Listener
macOS (arm64)ifm-station-darwin-arm64.dmgifm-relay-station-darwin-arm64.dmgifm-radio-listener-darwin-arm64.dmg
macOS (x64)ifm-station-darwin-x64.dmgifm-relay-station-darwin-x64.dmgifm-radio-listener-darwin-x64.dmg
Windows (x64)ifm-station-windows-x64.msiifm-relay-station-windows-x64.msiifm-radio-listener-windows-x64.msi
Windows (arm64)ifm-station-windows-arm64.msiifm-relay-station-windows-arm64.msiifm-radio-listener-windows-arm64.msi
Linux (x64)ifm-station-linux-x64.AppImageifm-relay-station-linux-x64.AppImageifm-radio-listener-linux-x64.AppImage
Linux (arm64)ifm-station-linux-arm64.AppImageifm-relay-station-linux-arm64.AppImageifm-radio-listener-linux-arm64.AppImage
Linux (deb)ifm-station_<ver>_amd64.debifm-relay-station_<ver>_amd64.debifm-radio-listener_<ver>_amd64.deb
Linux (rpm)ifm-station-<ver>.x86_64.rpmifm-relay-station-<ver>.x86_64.rpmifm-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 (uses crates/desktop)
  • src/ — React frontend (shared components with web apps)
  • tauri.conf.json — App config (identifier, permissions, updater)
  • package.json — npm scripts

Build Commands

bash
# 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 build

Release 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.txt

Cross-App Integration

ScenarioMechanism
Browser listener → Local stationStation exposes ws://127.0.0.1:<port>; listener connects via mDNS
Local relay ↔ Internet peersRelay announces on DHT; internet stations/Listeners connect via QUIC
Station → Multiple relaysStation selects relays from pool (mDNS + DHT)
Background relayRelay 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

Released under the MIT License.