Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.skyvexsoftware.com/llms.txt

Use this file to discover all available pages before exploring further.

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 background/index.js exists in the plugin’s install directory) 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.

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
  database: PluginDatabaseAccessor; // SQLite database scoped to plugin data directory
};
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: PluginPilotUser | null;
  };
  airline?: {
    id: string;
    name: string;
    icao: string;
    logo_light: string;
    logo_dark: string;
  } | null;
  config: {
    get<T>(key: string, defaultValue?: T): T;
  };
  socket: {
    connected: boolean;
    emit(event: string, data: unknown): void;
    on(event: string, handler: (data: unknown) => void): void;
    off(event: string, handler: (data: unknown) => void): void;
  };
  navigation: PluginNavigationHelper;
  toast: PluginToastAPI;          // success(), error(), info(), warning()
  logger: PluginLogger;
};

type PluginPilotUser = {
  dbID: number;
  pilotID: string;       // e.g. "QFA123"
  firstName: string;
  lastName: string;
  email: string;
  rank: string;
  rankLevel: number;
  rankImage: string;     // URL to rank insignia
  avatar: string;        // URL to pilot avatar
};
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. This means two plugins can both register a "get-status" handler without conflict. To communicate between your UI and background modules, register Express routes in the background module via ctx.server.registerRouter() and fetch them in the UI with TanStack Query. This keeps data flowing through standard HTTP rather than requiring a custom IPC abstraction.