Skip to main content
Stratos is built on a shell + plugin model. The shell owns the infrastructure — auth, routing, simulator connections, IPC bridge, theming, and the sidebar. Plugins are independently deployable modules that extend the shell with custom UI pages and/or background services.

Shell-Plugin Contract

The shell and plugins communicate through a strict, versioned contract. Plugins never import from the shell directly — all access goes through the SDK’s context objects and hooks, which the shell injects at runtime. Each plugin runs in its own host — errors in one plugin don’t crash others. IPC channels are automatically namespaced to plugin:{pluginId}:* to prevent collisions.

Plugin Lifecycle

Plugins go through four stages from discovery to teardown:

1. Discovery

On startup, the shell scans the plugins directory and reads each plugin’s plugin.json manifest. The sidebar order is determined by the VA owner’s configuration on the Skyvex platform.

2. Load

The shell loads the background module (if declared in the manifest) for each discovered plugin. This happens after auth is initialised, so the background module receives a fully populated PluginContext. If onStart() throws, the plugin is marked as errored. Other plugins continue loading normally.

3. Mount

When a pilot navigates to a plugin’s sidebar entry, the shell lazy-loads the UI module and mounts it inside a PluginShellProvider. This provider supplies PluginUIContext to all child components via React context. The UI module is unmounted when the pilot navigates away, but the background module keeps running.

4. Unmount / Stop

On shell shutdown, onStop() is called for each running background module. Use this to close connections, flush data, or cancel timers. After an OTA update, the shell calls onResume(ctx) on the background module if it’s defined — this lets you re-initialise without a full restart.

UI Module vs Background Module

UI ModuleBackground Module
Where it runsElectron renderer process (React)Electron main process (Node.js)
Entry pointsrc/ui/index.tsxsrc/background/index.ts
LifecycleMounted/unmounted on navigationRuns from startup to shutdown
ContextPluginUIContext via usePluginContext()PluginContext passed to onStart()
Use forDisplay, interaction, flight data visualisationExpress routes, IPC handlers, background tasks
Most plugins only need a UI module. Create a background module when you need persistent server-side logic — registering API routes, maintaining a WebSocket connection, or running scheduled tasks.

PluginContext

Passed to your background module’s onStart(ctx) function. Provides scoped access to shell infrastructure:
type PluginContext = {
  logger: PluginLogger;          // info/warn/error/debug — auto-prefixed with plugin ID
  config: PluginConfigStore;     // async key-value store namespaced to your plugin
  ipc: PluginIPCRegistrar;       // register IPC handlers (channels auto-prefixed)
  auth: PluginAuthAccessor;      // getToken(), isAuthenticated()
  server: PluginServerRegistrar; // register Express routers on the shell's HTTP server
};
All config keys are scoped to your plugin — you can’t read or write another plugin’s config. All IPC channels are prefixed as plugin:{pluginId}:* — you can’t accidentally handle another plugin’s messages.

PluginUIContext

Available in renderer components via usePluginContext(). Provides synchronous access to the same capabilities as PluginContext, plus navigation and UI utilities:
type PluginUIContext = {
  pluginId: string;
  auth: {
    isAuthenticated: boolean;
    token: string | null;
    user: object | null;
  };
  airline: {
    id: string;
    name: string;
    icao: string;
    logo_light: string;
    logo_dark: string;
  } | null;
  config: {
    get<T>(key: string, defaultValue?: T): T;
  };
  navigation: PluginNavigationHelper;
  toast: PluginToastAPI;          // success(), error(), info(), warning()
  logger: PluginLogger;
};
Note that config.get() is synchronous in the UI context (values are pre-loaded when the plugin mounts), while ctx.config.get() in the background context returns a Promise.

IPC Communication Model

The shell’s IPC bridge auto-prefixes all channel names to prevent cross-plugin pollution. When your background module registers a handler:
ctx.ipc.handle("get-status", () => ({ online: true }));
The actual IPC channel registered is plugin:my-plugin:get-status. Your UI module doesn’t need to know this — use the SDK’s usePluginIPC hook, which applies the same prefix automatically. This means two plugins can both register a "get-status" handler without conflict.