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
| Permission | Description | Risk |
|---|---|---|
broadcast | Send packets to mesh | Low |
send_to | Send to specific peer | Low |
storage | Persistent key-value store | Medium |
rpc | Register/call RPC methods | Medium |
identity | Access node identity/keys | High |
network | Raw transport access | High |
audio | Microphone/speaker access | High |
video | Camera access | High |
filesystem | File system access | High |
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
| Aspect | Core Plugins | External Plugins |
|---|---|---|
| Language | Rust | Rust, JS, WASM |
| Loading | Compile-time | Runtime |
| Permissions | Full (trusted) | Sandboxed |
| Distribution | Built into binary | npm, CDN, local |
| Updates | Node restart | Hot reload (JS) |
| Examples | Voice, Chat, File | Whiteboard, Games, AI |
Plugin Development Guide
1. Create Rust Plugin
bash
cargo new --lib ifm-plugin-myfeature
cd ifm-plugin-myfeatureCargo.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-pluginsrc/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 publishPlugin 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
- Minimal payload types — Only register for what you handle
- Idempotent handlers — Packets may be duplicated
- Async throughout — Never block in plugin callbacks
- Error isolation — Plugin crash shouldn't crash node
- Version negotiation — Handle protocol version differences
- Storage prefixes — Use
plugin:{id}:namespace - Clean shutdown — Implement
on_stopfor cleanup - Test offline — Plugins should work without network