TREK Plugin Runtime Architecture and Host RPC Communication: A Deep Dive
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) 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) 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) 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) 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.
// 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), and forks the child process:
const child = fork(entry, [sup.id, codeDir], { /* env + exec args */ });
The handshake protocol follows this sequence:
- The child emits
evt:helloupon startup - The supervisor responds with
evt:initcontaining decrypted configuration and allowed egress hosts - The child emits
evt:loadedafter completing its initialization - 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:
- Method lookup: The request is matched against
this.methodsin the RPC host - Permission check: If the plugin lacks the required permission (e.g.,
db:write:places), the method is not registered, resulting in anUNKNOWN_METHODerror - User context binding: The host derives the
actingUserIdfrom the invocation ID (_inv), preventing the plugin from forging user identities - Trip membership verification: Trip-scoped reads verify
canAccessTrip, while writes checkcanEditPlaces,canEditDays, or other relevant permissions - Schema validation: Input is validated against shared
@trek/sharedschemas 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.
// 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.
// 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, creating a traceable log of all capability access attempts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →