Skip to content

IFM Plugin System

Overview

The IFM plugin system is the primary extension mechanism — everything above the packet layer is a plugin. Voice, chat, video, file transfer, telemetry, gaming, AI agents — all are plugins. The core protocol only understands packets; plugins interpret payloads.


Plugin Architecture

Core Principle

The core protocol only knows about packets. Plugins define behavior.


Plugin Interface (Rust)

rust
use async_trait::async_trait;

#[async_trait]
pub trait Plugin: Send + Sync {
    /// Unique plugin identifier (FNV-1a hash of name)
    fn id(&self) -> u64;
    
    /// Human-readable name
    fn name(&self) -> &str;
    
    /// Version
    fn version(&self) -> &str;
    
    /// Payload types this plugin handles
    fn payload_types(&self) -> Vec<PayloadType>;
    
    /// Called when node starts
    async fn on_start(&mut self, context: &PluginContext) -> Result<()>;
    
    /// Called when node stops
    async fn on_stop(&mut self) -> Result<()>;
    
    /// Handle incoming packet
    async fn on_packet(&mut self, packet: &Packet, context: &PluginContext) -> Result<()>;
    
    /// Handle outgoing packet (can modify/inspect)
    async fn on_send(&mut self, packet: &mut Packet, context: &PluginContext) -> Result<()>;
    
    /// Periodic tick (e.g., every 100ms)
    async fn on_tick(&mut self, context: &PluginContext) -> Result<()>;
    
    /// Handle frequency tune/leave
    async fn on_frequency_change(&mut self, freq: &Frequency, joined: bool, context: &PluginContext) -> Result<()>;
    
    /// Handle peer join/leave
    async fn on_peer_change(&mut self, peer: &PeerId, connected: bool, context: &PluginContext) -> Result<()>;
}

Plugin Context

rust
pub struct PluginContext {
    /// Send a packet to the mesh
    pub broadcast: Box<dyn Fn(Packet) -> Result<()> + Send + Sync>,
    
    /// Send directly to a peer
    pub send_to: Box<dyn Fn(PeerId, Packet) -> Result<()> + Send + Sync>,
    
    /// Get current node info
    pub node_info: NodeInfo,
    
    /// Access persistent storage (plugin-scoped)
    pub storage: PluginStorage,
    
    /// Emit event to application layer
    pub emit: Box<dyn Fn(PluginEvent) + Send + Sync>,
    
    /// Register RPC method callable by other plugins/peers
    pub register_rpc: Box<dyn Fn(&str, RpcHandler) + Send + Sync>,
    
    /// Call RPC on another peer
    pub call_rpc: Box<dyn Fn(PeerId, &str, &[u8]) -> Result<Vec<u8>> + Send + Sync>,
}

Built-in Plugins

1. Voice Plugin

rust
pub struct VoicePlugin {
    encoder: OpusEncoder,
    decoder: OpusDecoder,
    jitter_buffer: JitterBuffer,
    capture: AudioCapture,
    playback: AudioPlayback,
    active: bool,
}

impl Plugin for VoicePlugin {
    fn id(&self) -> u64 { 0x1a2b3c4d }  // "voice"
    fn name(&self) -> &str { "voice" }
    fn payload_types(&self) -> Vec<PayloadType> { vec![PayloadType::VOICE] }
    
    async fn on_packet(&mut self, packet: &Packet, ctx: &PluginContext) -> Result<()> {
        if packet.payload_type == PayloadType::VOICE {
            self.jitter_buffer.push(packet.payload.clone(), packet.sequence);
        }
        Ok(())
    }
    
    async fn on_tick(&mut self, ctx: &PluginContext) -> Result<()> {
        if self.active {
            // Capture → Encode → Broadcast
            if let Some(frame) = self.capture.next_frame() {
                let encoded = self.encoder.encode(&frame)?;
                let packet = Packet::voice(encoded, self.next_sequence());
                ctx.broadcast(packet)?;
            }
            
            // Jitter buffer → Decode → Playback
            if let Some(frame) = self.jitter_buffer.pop() {
                let decoded = self.decoder.decode(&frame)?;
                self.playback.push(&decoded);
            }
        }
        Ok(())
    }
}

2. Chat Plugin

rust
pub struct ChatPlugin {
    channels: HashMap<String, ChannelState>,
    history: MessageHistory,
}

impl Plugin for ChatPlugin {
    fn id(&self) -> u64 { 0x5e6f7a8b }  // "chat"
    fn name(&self) -> &str { "chat" }
    fn payload_types(&self) -> Vec<PayloadType> { 
        vec![PayloadType::TEXT, PayloadType::JSON, PayloadType::CONTROL]
    }
    
    async fn on_packet(&mut self, packet: &Packet, ctx: &PluginContext) -> Result<()> {
        match packet.payload_type {
            PayloadType::TEXT => {
                let text = String::from_utf8(packet.payload)?;
                self.history.add(Message::text(packet.sender, text));
                ctx.emit(PluginEvent::ChatMessage { ... })?;
            }
            PayloadType::JSON => {
                let msg: ChatMessage = serde_json::from_slice(&packet.payload)?;
                self.handle_chat_message(msg, packet.sender)?;
            }
            PayloadType::CONTROL => {
                let ctrl: ControlMessage = serde_json::from_slice(&packet.payload)?;
                self.handle_control(ctrl, packet.sender)?;
            }
        }
        Ok(())
    }
}

3. File Transfer Plugin

rust
pub struct FilePlugin {
    transfers: HashMap<FileId, FileTransfer>,
    chunk_size: usize,
}

impl Plugin for FilePlugin {
    fn id(&self) -> u64 { 0x9c0d1e2f }  // "file"
    fn name(&self) -> &str { "file" }
    fn payload_types(&self) -> Vec<PayloadType> { vec![PayloadType::FILE] }
    
    async fn on_packet(&mut self, packet: &Packet, ctx: &PluginContext) -> Result<()> {
        let chunk: FileChunk = deserialize(&packet.payload)?;
        let transfer = self.transfers.entry(chunk.file_id).or_insert_with(|| {
            FileTransfer::new(chunk.file_id, chunk.total_chunks)
        });
        transfer.receive_chunk(chunk)?;
        
        if transfer.is_complete() {
            let data = transfer.reassemble();
            ctx.emit(PluginEvent::FileReceived { 
                file_id: chunk.file_id,
                filename: transfer.filename(),
                data,
            })?;
        }
        Ok(())
    }
}

JavaScript Plugin SDK

Plugin Definition (TypeScript)

typescript
interface IFMPlugin {
  id: string;                    // Unique identifier
  name: string;                  // Human-readable name
  version: string;               // Semver
  payloadTypes: PayloadType[];   // Which packet types to receive
  
  // Lifecycle
  onStart?(ctx: PluginContext): Promise<void>;
  onStop?(ctx: PluginContext): Promise<void>;
  
  // Packet handling
  onPacket?(packet: Packet, ctx: PluginContext): Promise<void>;
  onSend?(packet: Packet, ctx: PluginContext): Promise<void>;
  
  // Events
  onFrequencyChange?(freq: Frequency, joined: boolean, ctx: PluginContext): Promise<void>;
  onPeerChange?(peer: PeerId, connected: boolean, ctx: PluginContext): Promise<void>;
  onTick?(ctx: PluginContext): Promise<void>;
}

// Registration
radio.use(new ChatPlugin());
radio.use(new VoicePlugin());
radio.use(new MyCustomPlugin());

Plugin Context (JavaScript)

typescript
interface PluginContext {
  // Send to mesh
  broadcast(packet: Packet): Promise<void>;
  
  // Send to specific peer
  sendTo(peerId: PeerId, packet: Packet): Promise<void>;
  
  // Current node info
  nodeInfo: NodeInfo;
  
  // Plugin-scoped persistent storage
  storage: PluginStorage;
  
  // Emit event to application
  emit(event: PluginEvent): void;
  
  // RPC
  registerRPC(method: string, handler: RPCHandler): void;
  callRPC(peerId: PeerId, method: string, args: Uint8Array): Promise<Uint8Array>;
}

Example: Custom Plugin (TypeScript)

typescript
class WhiteboardPlugin implements IFMPlugin {
  id = "whiteboard";
  name = "Whiteboard";
  version = "1.0.0";
  payloadTypes = [PayloadType.PLUGIN];
  
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  
  async onStart(ctx: PluginContext) {
    // Register RPC for sync
    ctx.registerRPC("whiteboard:draw", this.handleDraw.bind(this));
    ctx.registerRPC("whiteboard:clear", this.handleClear.bind(this));
  }
  
  async onPacket(packet: Packet, ctx: PluginContext) {
    if (packet.payloadType === PayloadType.PLUGIN) {
      const pluginPacket = decodePluginPacket(packet.payload);
      if (pluginPacket.pluginId === this.id) {
        this.handlePluginPacket(pluginPacket);
      }
    }
  }
  
  private handleDraw(data: Uint8Array) {
    const cmd = decodeDrawCommand(data);
    this.drawCommand(cmd);
  }
  
  private handleClear() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }
  
  // Public API for app
  drawLine(x1: number, y1: number, x2: number, y2: number, color: string) {
    const cmd = encodeDrawCommand({ type: 'line', x1, y1, x2, y2, color });
    this.broadcastPluginPacket(cmd);
  }
  
  private broadcastPluginPacket(data: Uint8Array) {
    const packet = new Packet({
      payloadType: PayloadType.PLUGIN,
      payload: encodePluginPacket(this.id, data),
    });
    this.ctx.broadcast(packet);
  }
}

Plugin Discovery & Loading

Rust (Compile-time)

rust
// In ifm-node/Cargo.toml
[dependencies]
ifm-voice = { path = "../crates/audio" }
ifm-chat = { path = "../plugins/chat" }
ifm-video = { path = "../plugins/video" }

// In node builder
let node = Node::builder()
    .plugin(VoicePlugin::new())
    .plugin(ChatPlugin::new())
    .plugin(VideoPlugin::new())
    .build();

JavaScript (Runtime)

typescript
// Dynamic loading
const pluginModule = await import('./my-plugin.js');
const plugin = new pluginModule.MyPlugin();
radio.use(plugin);

// From npm
import { WhiteboardPlugin } from '@ifm/plugin-whiteboard';
radio.use(new WhiteboardPlugin());

// From URL (browser)
const plugin = await loadPlugin('https://plugins.ifm.network/whiteboard.js');
radio.use(plugin);

Plugin Manifest (package.json)

json
{
  "name": "@ifm/plugin-whiteboard",
  "version": "1.0.0",
  "ifm": {
    "plugin": {
      "entry": "dist/index.js",
      "id": "whiteboard",
      "name": "Whiteboard",
      "payloadTypes": ["PLUGIN"],
      "permissions": ["storage", "rpc", "broadcast"],
      "minCoreVersion": "0.1.0"
    }
  },
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

Plugin Permissions

PermissionDescriptionRisk
broadcastSend packets to meshLow
send_toSend to specific peerLow
storagePersistent key-value storeMedium
rpcRegister/call RPC methodsMedium
identityAccess node identity/keysHigh
networkRaw transport accessHigh
audioMicrophone/speaker accessHigh
videoCamera accessHigh
filesystemFile system accessHigh

Permission Model

rust
struct PluginPermissions {
    broadcast: bool,
    send_to: bool,
    storage: bool,
    rpc: bool,
    identity: bool,
    network: bool,
    audio: bool,
    video: bool,
    filesystem: bool,
}

// Default: only broadcast + storage
impl Default for PluginPermissions {
    fn default() -> Self {
        Self {
            broadcast: true,
            send_to: true,
            storage: true,
            rpc: true,
            identity: false,
            network: false,
            audio: false,
            video: false,
            filesystem: false,
        }
    }
}

Plugin Communication Patterns

1. Broadcast (Pub/Sub)

2. Direct (Peer-to-Peer)

3. RPC (Request/Response)

4. Events (Local)


Plugin Storage

Each plugin gets an isolated key-value store:

rust
trait PluginStorage {
    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
    async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
    async fn delete(&self, key: &str) -> Result<()>;
    async fn list(&self, prefix: &str) -> Result<Vec<String>>;
    async fn clear(&self) -> Result<()>;
}

// Namespaced by plugin ID: "plugin:{plugin_id}:{key}"

JavaScript

typescript
interface PluginStorage {
  get(key: string): Promise<Uint8Array | null>;
  set(key: string, value: Uint8Array): Promise<void>;
  delete(key: string): Promise<void>;
  list(prefix: string): Promise<string[]>;
  clear(): Promise<void>;
}

Core Plugins vs External Plugins

AspectCore PluginsExternal Plugins
LanguageRustRust, JS, WASM
LoadingCompile-timeRuntime
PermissionsFull (trusted)Sandboxed
DistributionBuilt into binarynpm, CDN, local
UpdatesNode restartHot reload (JS)
ExamplesVoice, Chat, FileWhiteboard, Games, AI

Plugin Development Guide

1. Create Rust Plugin

bash
cargo new --lib ifm-plugin-myfeature
cd ifm-plugin-myfeature

Cargo.toml:

toml
[package]
name = "ifm-plugin-myfeature"
version = "0.1.0"
edition = "2021"

[dependencies]
ifm-protocol = { path = "../../../crates/protocol" }
ifm-core = { path = "../../../crates/core" }
async-trait = "0.1"
serde = { version = "1.0", features = ["derive"] }

src/lib.rs:

rust
use ifm_protocol::{Packet, PayloadType, Frequency, PeerId};
use ifm_core::{Plugin, PluginContext, Result};

pub struct MyFeaturePlugin { ... }

#[async_trait]
impl Plugin for MyFeaturePlugin {
    fn id(&self) -> u64 { 0x_my_feature_hash }
    fn name(&self) -> &str { "myfeature" }
    fn payload_types(&self) -> Vec<PayloadType> { vec![PayloadType::PLUGIN] }
    
    async fn on_packet(&mut self, packet: &Packet, ctx: &PluginContext) -> Result<()> {
        // Handle packets
        Ok(())
    }
}

2. Create JavaScript Plugin

bash
npm create ifm-plugin@latest my-plugin
cd my-plugin

src/index.ts:

typescript
import { IFMPlugin, PluginContext, Packet, PayloadType } from '@ifm/sdk';

export class MyPlugin implements IFMPlugin {
  id = "myplugin";
  name = "My Plugin";
  version = "1.0.0";
  payloadTypes = [PayloadType.PLUGIN];
  
  async onPacket(packet: Packet, ctx: PluginContext) {
    // Handle packets
  }
}

export default MyPlugin;

3. Publish

bash
# Rust
cargo publish

# JavaScript
npm publish

Plugin API Reference (Events)

Emitted by Plugins → Application

typescript
type PluginEvent =
  | { type: 'chat:message'; message: ChatMessage }
  | { type: 'voice:start'; peer: PeerId }
  | { type: 'voice:stop'; peer: PeerId }
  | { type: 'voice:level'; peer: PeerId; level: number }
  | { type: 'file:start'; transfer: FileTransfer }
  | { type: 'file:progress'; transfer: FileTransfer; progress: number }
  | { type: 'file:complete'; transfer: FileTransfer; data: Uint8Array }
  | { type: 'peer:join'; peer: PeerId; frequencies: Frequency[] }
  | { type: 'peer:leave'; peer: PeerId }
  | { type: 'frequency:join'; frequency: Frequency; peers: PeerId[] }
  | { type: 'frequency:leave'; frequency: Frequency }
  | { type: 'custom'; pluginId: string; event: string; data: any };

Application Listens

typescript
radio.on('chat:message', (msg) => console.log(msg));
radio.on('voice:start', (peer) => showSpeakingIndicator(peer));
radio.on('file:complete', (transfer, data) => saveFile(transfer.filename, data));

Mermaid: Plugin System Flow


Best Practices

  1. Minimal payload types — Only register for what you handle
  2. Idempotent handlers — Packets may be duplicated
  3. Async throughout — Never block in plugin callbacks
  4. Error isolation — Plugin crash shouldn't crash node
  5. Version negotiation — Handle protocol version differences
  6. Storage prefixes — Use plugin:{id}: namespace
  7. Clean shutdown — Implement on_stop for cleanup
  8. Test offline — Plugins should work without network

Released under the MIT License.