Skip to main content
Background modules run in the Electron main process (Node.js). They’re optional — only add one when you need persistent server-side logic that outlives UI navigation: registering Express routes, maintaining a connection, or running scheduled tasks.

When to Use a Background Module

Use a background module when you need to:
  • Expose an HTTP API to your UI module (via Express routes)
  • Handle IPC messages from the renderer
  • Maintain a persistent connection (WebSocket, database, etc.)
  • Run background polling or scheduled work
  • Access Node.js APIs not available in the renderer
If your plugin only displays data and reacts to user interaction, a UI module alone is sufficient.

Declaring a Background Module

Add the background field to your plugin.json:
{
  "background": {
    "entryModule": "background/index.ts"
  }
}
The shell will load this module on startup and call onStart(ctx) after auth is initialised.

The PluginBackgroundModule Contract

Your background entry module must export onStart and onStop. Use the createPlugin helper for type safety and validation:
import { createPlugin } from "@skyvexsoftware/stratos-sdk";

export default createPlugin({
  async onStart(ctx) {
    ctx.logger.info("MyPlugin", "Starting up...");
    // register routes, set up IPC handlers, initialise services
  },

  async onStop() {
    // clean up: close connections, cancel timers, flush data
  },

  // Optional: called after an OTA update instead of a full restart
  async onResume(ctx) {
    ctx.logger.info("MyPlugin", "Resuming after update");
    // re-initialise with fresh context
  },
});
Or export named functions directly:
import type { PluginContext } from "@skyvexsoftware/stratos-sdk/types";

export async function onStart(ctx: PluginContext): Promise<void> {
  ctx.logger.info("MyPlugin", "Starting...");
}

export async function onStop(): Promise<void> {
  // teardown
}

PluginContext Fields

The ctx object passed to onStart() provides scoped access to shell infrastructure:

ctx.loggerPluginLogger

Standard log methods, automatically prefixed with your plugin ID in the log output:
ctx.logger.info("MyPlugin", "Server ready");
ctx.logger.warn("MyPlugin", "Retrying connection...");
ctx.logger.error("MyPlugin", "Failed to fetch fleet data", error);
ctx.logger.debug("MyPlugin", "Polling interval set", { interval });

ctx.configPluginConfigStore

Persistent key-value store namespaced to your plugin. All methods are async. Values survive shell restarts:
const interval = await ctx.config.get<number>("refresh_interval", 30);
const links = await ctx.config.get<object[]>("quick_links", []);

await ctx.config.set("last_sync", Date.now());
await ctx.config.delete("stale_key");

const all = await ctx.config.getAll();
Note: config.get() is async in the background context. In the UI context (via usePluginContext()), it’s synchronous.

ctx.ipcPluginIPCRegistrar

Register IPC handlers and send messages to the renderer. All channel names are automatically prefixed as plugin:{pluginId}:*:
// Handle requests from the renderer
ctx.ipc.handle("get-status", async () => {
  return { online: true, pilots: 42 };
});

// Push data to the renderer unprompted
ctx.ipc.send("new-flight-available", { flightId: "VA123" });
The actual channels registered are plugin:my-plugin:get-status and plugin:my-plugin:new-flight-available — the prefix is applied transparently.

ctx.authPluginAuthAccessor

Read-only access to the current authentication state:
const isAuth = ctx.auth.isAuthenticated();
const token = ctx.auth.getToken(); // string | null
Use this to make authenticated requests to your VA’s API from the background module.

ctx.serverPluginServerRegistrar

Register Express routers on the shell’s internal HTTP server (port 2066):
import { Router } from "express";

const router = Router();

router.get("/status", (req, res) => {
  res.json({ ok: true });
});

export async function onStart(ctx) {
  ctx.server.registerRouter("/api/my-plugin", router);
}
Routes are available at http://127.0.0.1:2066/api/my-plugin/status. Use TanStack Query in your UI module to fetch from these endpoints.

Registering Express Routes

A common pattern is to fetch data from your VA’s external API in the background module and expose it locally:
import { Router } from "express";
import type { PluginContext } from "@skyvexsoftware/stratos-sdk/types";

const router = Router();

router.get("/fleet", async (req, res) => {
  try {
    const response = await fetch("https://api.my-va.com/fleet");
    const data = await response.json();
    res.json({ success: true, data });
  } catch (err) {
    res.status(500).json({ success: false, error: "Failed to fetch fleet" });
  }
});

export async function onStart(ctx: PluginContext) {
  ctx.server.registerRouter("/api/my-plugin", router);
  ctx.logger.info("MyPlugin", "Routes registered");
}

export async function onStop() {
  // nothing to clean up for a simple router
}
In your UI module, query this endpoint with TanStack Query:
import { useQuery } from "@tanstack/react-query";

function FleetList() {
  const { data, isLoading } = useQuery({
    queryKey: ["fleet"],
    queryFn: () =>
      fetch("http://127.0.0.1:2066/api/my-plugin/fleet").then((r) => r.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  return <ul>{data?.data?.map((f) => <li key={f.id}>{f.name}</li>)}</ul>;
}

IPC Between Background and UI

Use IPC for real-time push from the background to the UI — data the UI didn’t ask for, like live updates or alerts:
// Background: push an update to the renderer
ctx.ipc.send("status-update", { connected: true, pilots: 42 });
// UI: listen for background events
import { usePluginIPC } from "@skyvexsoftware/stratos-sdk";

function StatusBanner() {
  const [status, setStatus] = useState(null);

  usePluginIPC("status-update", (payload) => {
    setStatus(payload);
  });

  return status ? <p>{status.pilots} pilots online</p> : null;
}
For request-response patterns (UI asks background for data), prefer Express routes over IPC — they compose naturally with TanStack Query.

Lifecycle Summary

  1. Shell starts and initialises auth
  2. Shell calls onStart(ctx) for each plugin with a background module
  3. If onStart() throws, the plugin is marked errored — other plugins still load
  4. On shutdown, shell calls onStop() for each running background module
  5. After an OTA update, shell calls onResume(ctx) if defined, otherwise calls onStop() then onStart(ctx) fresh