# TREK Plugin Runtime Architecture and Host RPC Communication: A Deep Dive

> Explore TREK's plugin runtime architecture and host RPC communication. Learn how plugins run in isolated Node.js processes for secure, permission-gated host interaction.

- Repository: [Maurice/TREK](https://github.com/mauriceboe/TREK)
- Tags: deep-dive
- Published: 2026-07-11

---

**TREK runs each plugin in an isolated Node.js child process, using a supervisor-based architecture with permission-gated host RPC communication to securely expose core capabilities while maintaining strict isolation between plugins and the host system.**

The TREK plugin system (from the `mauriceboe/TREK` repository) implements a sandboxed runtime that allows third-party extensions to interact with trip data, places, and WebSocket events without compromising server stability. Understanding how the **plugin runtime architecture** coordinates child processes and how **host RPC communication** validates every request is essential for developing secure, performant plugins.

## Core Components of the Plugin Runtime

The architecture separates concerns across four primary services, each handling specific aspects of plugin lifecycle management and capability routing.

### PluginRuntimeService

The `PluginRuntimeService` (located in [`server/src/nest/plugins/plugins.service.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/plugins/plugins.service.ts)) serves as the orchestration layer. It discovers plugins on the volume, resolves dependency order, and delegates activation and deactivation to the supervisor. This service also maintains the encrypted configuration and granted permission sets for each plugin instance.

### PluginSupervisor

The `PluginSupervisor` ([`server/src/nest/plugins/plugin-supervisor.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/plugins/plugin-supervisor.ts)) owns the lifecycle of every child process. It forks the plugin process, monitors heartbeats, enforces memory limits, and implements exponential back-off for crash recovery. The supervisor maintains route tables, hooks, exports, and subscription states for each active plugin.

### PluginRpcHost

The `PluginRpcHost` ([`server/src/nest/plugins/host/rpc-host.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/plugins/host/rpc-host.ts)) functions as the **host-side capability router**. When a plugin requests a core capability—such as reading trips, creating costs, or broadcasting WebSocket events—this component validates the request against the plugin's granted permission set, verifies trip membership and role requirements, and forwards the request to the appropriate core service.

### PluginEventSink

The `PluginEventSink` ([`server/src/plugin-event-sink.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/plugin-event-sink.ts)) provides a fire-and-forget mechanism for forwarding trip-level events from the core to plugins that have subscribed via `events:subscribe`.

## How the Plugin Runtime Lifecycle Works

The runtime follows a structured sequence from discovery to active operation, ensuring dependencies are respected and each plugin is properly isolated.

### Discovery and Activation

When the NestJS module initializes (`PluginRuntimeService.onModuleInit`), the system scans the plugins directory and registers discovered plugins as inactive. It then activates every plugin marked as enabled, respecting the dependency order defined in the plugin manifest.

```typescript
// From server/src/nest/plugins/plugins.service.ts
const installed = this.installedDepRows();
const enabledIds = [...installed.values()].filter(r => r.enabled).map(r => r.id);
const order = enableOrder(enabledIds, installed);
for (const id of order) this.activate(id).catch(() => {});

```

The `enableOrder` function ensures that dependencies are activated before their dependants, preventing runtime errors from missing capabilities.

### Process Spawning and Handshake

The `PluginSupervisor.activate` method creates a `Supervised` record, builds a `PluginRpcHost` via `createRealRpcHost` ([`server/src/nest/plugins/host/create-rpc-host.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/plugins/host/create-rpc-host.ts)), and forks the child process:

```typescript
const child = fork(entry, [sup.id, codeDir], { /* env + exec args */ });

```

The handshake protocol follows this sequence:
1. The child emits `evt:hello` upon startup
2. The supervisor responds with `evt:init` containing decrypted configuration and allowed egress hosts
3. The child emits `evt:loaded` after completing its initialization
4. The supervisor records the plugin's routes, hooks, and exports, resolving the activation promise

### Runtime Monitoring and Crash Recovery

The supervisor continuously monitors each plugin process through heartbeat checks and RSS memory tracking. If a plugin exceeds memory limits or crashes, the supervisor applies exponential back-off restart logic. After repeated crashes, the plugin is automatically disabled to prevent system degradation.

## Host RPC Communication Flow

Host RPC communication provides the secure bridge between sandboxed plugins and core TREK services, enforcing permissions at every boundary.

### Request Dispatch and Permission Validation

When a plugin calls a host method, the child sends a request envelope: `{ k: 'req', method: 'places.create', params: {...} }`. The supervisor forwards this to `PluginRpcHost.dispatch`, which executes the following validation sequence:

1. **Method lookup**: The request is matched against `this.methods` in the RPC host
2. **Permission check**: If the plugin lacks the required permission (e.g., `db:write:places`), the method is not registered, resulting in an `UNKNOWN_METHOD` error
3. **User context binding**: The host derives the `actingUserId` from the invocation ID (`_inv`), preventing the plugin from forging user identities
4. **Trip membership verification**: Trip-scoped reads verify `canAccessTrip`, while writes check `canEditPlaces`, `canEditDays`, or other relevant permissions
5. **Schema validation**: Input is validated against shared `@trek/shared` schemas before delegation to core services

The host then returns a response envelope: `{ k: 'res', ok: true, result }`, which the supervisor matches to the pending promise created by the original `invoke` call.

### Inter-Plugin Communication

Plugins can invoke each other using `plugins.call`. The host-side router validates that the dependency edge is satisfied (`dependsOnSatisfied`) and that the target plugin exports the requested function (`exportsOf`). The call is then forwarded to the target's child process through the supervisor's `invoke` mechanism.

```typescript
// Plugin-side code
await ctx.plugins.call('places.create', {
  tripId: 42,
  input: { name: 'Eiffel Tower', address: 'Paris' },
});

```

### Event Broadcasting

When emitting events, the host routes messages to all subscribed plugins. Event names are prefixed with `plugin:{id}:` to prevent namespace collisions in the WebSocket layer.

```typescript
// Core service emitting event
await this.supervisor.deliverEvent(tripId, 'place:created');

```

The supervisor iterates over active plugins with `events:subscribe` permissions, invoking each plugin's event handler via the RPC channel.

## Security Model and Isolation Guarantees

The architecture implements defense in depth through multiple isolation layers.

### Permission-Gated Method Registration

The `PluginRpcHost` only registers methods corresponding to the plugin's explicitly granted permissions. If a plugin lacks `db:write:places`, the method simply does not exist in the host's method table, preventing capability discovery attacks.

### User and Trip Context Enforcement

Every read and write operation is bound to the host-derived `actingUserId`. The child process cannot specify a different user ID because the host looks up the user from the invocation context. Trip-scoped operations additionally verify membership and role-based edit permissions.

### Resource Limits and Crash Isolation

Plugins operate under strict memory limits monitored by the supervisor. Outbound HTTP requests are restricted to hosts explicitly listed in the plugin's `granted_permissions` (`http:outbound:<host>`). This ensures that a compromised or misbehaving plugin cannot exhaust server resources or make unauthorized network calls.

## Summary

- **TREK's plugin runtime** isolates each extension in a Node.js child process managed by `PluginSupervisor`, with automatic restart and memory monitoring.
- **Host RPC communication** routes all plugin requests through `PluginRpcHost`, which enforces permission checks, schema validation, and trip-membership verification before accessing core services.
- **Security guarantees** include permission-gated method registration, host-derived user context binding, and outbound network restrictions based on explicit grants.
- **Inter-plugin communication** validates dependency edges and exports, while event broadcasting uses prefixed namespaces to avoid collisions.
- **Crash isolation** ensures that plugin failures cannot destabilize the TREK server through supervised process lifecycle management.

## Frequently Asked Questions

### How does TREK prevent plugins from accessing data belonging to other users?

The `PluginRpcHost` binds every request to an `actingUserId` derived from the host's invocation context (`_inv`). The plugin cannot forge a different user ID because the host looks up the user from the internal invocation ID. Additionally, trip-scoped operations verify `canAccessTrip` before returning any data, ensuring users only access trips they are members of.

### What happens if a plugin crashes or consumes too much memory?

The `PluginSupervisor` monitors each child process through heartbeat checks and RSS memory tracking. If a plugin crashes, the supervisor applies exponential back-off restart logic. If memory limits are exceeded or crashes repeat, the plugin is automatically disabled. This guarantees that a single plugin cannot bring down the entire TREK server.

### Can plugins communicate directly with each other without host mediation?

No, direct inter-plugin communication is prohibited. When a plugin calls `ctx.plugins.call`, the request flows through the host's `PluginSupervisor`, which validates that the calling plugin has declared a dependency on the target plugin (`dependsOnSatisfied`) and that the target actually exports the requested function (`exportsOf`). The host then forwards the call through the target's RPC channel.

### How does TREK validate that a plugin is allowed to use specific core features?

The `PluginRpcHost` maintains a method table populated only with capabilities corresponding to the plugin's granted permission set. For example, if a plugin lacks `db:write:places`, the `places.create` method is not registered in the host's router, causing the request to return `UNKNOWN_METHOD`. Additionally, the `createRealRpcHost` factory wires the host to audit every RPC call via [`server/src/nest/plugins/host/plugin-audit.ts`](https://github.com/mauriceboe/TREK/blob/main/server/src/nest/plugins/host/plugin-audit.ts), creating a traceable log of all capability access attempts.