Skip to main content

Event Routing

The server runtime implements a flexible event routing system that determines how events are distributed to connected peers.

Routing Flow

Route Middleware

Middleware functions intercept events and make routing decisions.

Middleware Interface

type RouteMiddleware = (context: RouteContext) => RouteDecision | void

interface RouteContext {
  event: WebSocketEvent
  fromPeer: AuthenticatedPeer
  peers: Map<string, AuthenticatedPeer>
  destinations?: Array<string | RouteTargetExpression>
}

type RouteDecision
  = | { type: 'drop' }           // Discard the event
    | { type: 'broadcast' }      // Send to all peers
    | { type: 'targets', targetIds: Set<string> }  // Send to specific peers

Custom Middleware Example

import { setupApp, RouteMiddleware } from '@proj-airi/server-runtime'

// Only allow events from specific plugins
const pluginFilterMiddleware: RouteMiddleware = (context) => {
  const pluginId = context.fromPeer.identity?.plugin?.id
  
  if (!pluginId || !['allowed-plugin-1', 'allowed-plugin-2'].includes(pluginId)) {
    return { type: 'drop' }
  }
  
  // Return void to continue to next middleware
}

// Rate limiting middleware
const rateLimiters = new Map<string, { count: number, resetAt: number }>()

const rateLimitMiddleware: RouteMiddleware = (context) => {
  const peerId = context.fromPeer.peer.id
  const now = Date.now()
  const limit = rateLimiters.get(peerId)
  
  if (!limit || now > limit.resetAt) {
    rateLimiters.set(peerId, { count: 1, resetAt: now + 60000 })
    return
  }
  
  if (limit.count >= 100) {
    return { type: 'drop' }
  }
  
  limit.count++
}

const { app } = setupApp({
  routing: {
    middleware: [pluginFilterMiddleware, rateLimitMiddleware]
  }
})

Routing Policies

Policies provide declarative access control without custom middleware code.

Policy Configuration

interface RoutingPolicy {
  allowPlugins?: string[]    // Whitelist of plugin IDs
  denyPlugins?: string[]     // Blacklist of plugin IDs
  allowLabels?: string[]     // Required labels (any match)
  denyLabels?: string[]      // Forbidden labels (any match)
}

Policy Examples

Plugin Whitelist

const { app } = setupApp({
  routing: {
    policy: {
      allowPlugins: ['core-module', 'trusted-plugin']
    }
  }
})

Label-Based Access Control

const { app } = setupApp({
  routing: {
    policy: {
      allowLabels: ['tier=premium', 'env=production'],
      denyLabels: ['deprecated=true']
    }
  }
})

Combined Policy

const { app } = setupApp({
  routing: {
    policy: {
      allowPlugins: ['core-module', 'ai-module'],
      denyPlugins: ['legacy-module'],
      allowLabels: ['tier=premium'],
      denyLabels: ['beta=true', 'deprecated=true']
    }
  }
})

Route Destinations

Events can specify destinations using module names or label selectors.

Destination Types

type RouteTargetExpression = {
  name?: string                    // Module name
  index?: number                   // Module instance index
  labels?: Record<string, string>  // Label selectors
}

Specifying Destinations

By Module Name

client.send({
  type: 'custom:event',
  data: { message: 'Hello' },
  route: {
    destinations: ['ai-module', 'chat-module']
  }
})

By Module Instance

client.send({
  type: 'custom:event',
  data: { message: 'Hello' },
  route: {
    destinations: [
      { name: 'ai-module', index: 0 },
      { name: 'ai-module', index: 1 }
    ]
  }
})

By Labels

client.send({
  type: 'custom:event',
  data: { message: 'Hello' },
  route: {
    destinations: [
      { labels: { role: 'processor' } },
      { labels: { tier: 'premium', region: 'us-east' } }
    ]
  }
})

In Event Data

Destinations can also be specified in the data payload:
client.send({
  type: 'custom:event',
  data: {
    message: 'Hello',
    destinations: ['ai-module']
  }
})

Bypass Mode

Devtools peers can bypass routing policies for debugging and monitoring.

Enabling Bypass

A peer is considered a devtools peer if:
  • It has the label devtools=true or devtools=1, OR
  • Its module name contains “devtools”
// Module with devtools label
client.send({
  type: 'module:announce',
  data: {
    name: 'monitoring-tool',
    identity: {
      kind: 'plugin',
      plugin: { id: 'monitoring', labels: { devtools: 'true' } },
      id: 'monitor-1'
    }
  }
})

// Send event with bypass
client.send({
  type: 'monitor:observe',
  data: { target: 'all' },
  route: {
    bypass: true  // Only works for devtools peers
  }
})

Disabling Bypass

const { app } = setupApp({
  routing: {
    allowBypass: false  // Disable bypass even for devtools
  }
})

Label Matching

Label selectors support exact matching with AND logic.
// Peer labels
const peerLabels = {
  tier: 'premium',
  region: 'us-east',
  env: 'production'
}

// Match: all selector labels must match peer labels
const selector1 = { tier: 'premium' }  // ✓ Matches
const selector2 = { tier: 'premium', region: 'us-east' }  // ✓ Matches
const selector3 = { tier: 'free' }  // ✗ No match
const selector4 = { tier: 'premium', region: 'eu-west' }  // ✗ No match

Built-in Routing Rules

Self-Exclusion

Events are never sent back to the originating peer:
// Peer A sends event
client.send({
  type: 'broadcast:message',
  data: { text: 'Hello' }
})

// Server sends to all peers EXCEPT Peer A

Module Registry Access

The routing context provides access to the module registry:
const customMiddleware: RouteMiddleware = (context) => {
  const targetIds = new Set<string>()
  
  for (const [id, peer] of context.peers) {
    if (peer.name === 'ai-module') {
      targetIds.add(id)
    }
  }
  
  return { type: 'targets', targetIds }
}

Performance Considerations

Keep middleware functions lightweight. Heavy computation or I/O operations will block event processing.
Built-in policies are optimized for common access control patterns. Use custom middleware only when policies are insufficient.
If performing complex label matching, cache results within middleware to avoid repeated computation.
High-frequency events benefit from explicit destinations rather than broadcast-then-filter approaches.

Debugging Routing

Enable WebSocket logging to trace routing decisions:
const { app } = setupApp({
  logger: {
    websocket: {
      level: 'debug',
      format: 'pretty'
    }
  }
})
Log output includes:
  • Received events with peer information
  • Routing decisions (broadcast, targets, drop)
  • Delivery status to each peer
  • Connection health (heartbeat, timeout)

Next Steps

WebSocket Protocol

Review the WebSocket message protocol

Server SDK

Use the client SDK with built-in routing

Build docs developers (and LLMs) love