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.

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 entry to your Vite config:
import { createPluginConfig } from "@skyvexsoftware/stratos-sdk/vite";
import tailwindcss from "@tailwindcss/vite";

export default createPluginConfig({
  ui: { entry: "src/ui/index.tsx" },
  background: { entry: "src/background/index.ts" },
  vite: {
    plugins: [tailwindcss()],
  },
});
The shell discovers background modules automatically by checking for the compiled background/index.js in your plugin’s install directory — no plugin.json configuration is needed.

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/helpers";

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

  async onStop(ctx) {
    ctx.logger.info("MyPlugin", "Shutting down...");
    // clean up: close connections, cancel timers, flush data
  },

});
The createPlugin helper validates your module at import time and gives you full type inference on ctx — no manual type imports needed. The shell requires this default export pattern; named exports are not supported.

PluginContext Fields

The ctx object passed to onStart() and onStop() 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 = await ctx.auth.isAuthenticated();
const token = await ctx.auth.getToken(); // string | null
For HTTP calls to the VA, prefer ctx.airline.createClient() below — it adds refresh-on-401 handling that ctx.auth.getToken() does not.

ctx.airlinePluginAirlineAccessor

Read-only access to the currently-bound airline plus a pre-configured HTTP client:
type PluginAirlineAccessor = {
  getCurrent(): Promise<PluginAirline | null>;
  createClient(): AxiosInstance;
};

type PluginAirline = {
  id: string;        // airline UUID as Stratos knows it
  baseUrl: string;   // origin of the VA's API (no trailing slash)
  token: string | null;
};

getCurrent()

Returns the current airline binding. Re-call it when you need a fresh snapshot — there’s no event subscription.
const airline = await ctx.airline.getCurrent();
ctx.logger.info("MyPlugin", `Bound to ${airline.id} at ${airline.baseUrl}`);
Returns an axios instance pre-configured for the bound airline:
  • baseURL is set to airline.baseUrl (resolved per request).
  • Authorization header carries the current VA bearer.
  • On a 401 response, the shell exchanges the stored refresh token at the VA’s refresh_url. If refresh succeeds, the request is retried exactly once. If the VA didn’t configure a refresh_url, the user has no refresh token, or the VA rejected the refresh, the 401 propagates and the shell’s existing re-auth flow takes over.
Cache the returned instance for the lifetime of the background module — it’s safe to reuse across requests.
import { createPlugin } from "@skyvexsoftware/stratos-sdk/helpers";

export default createPlugin({
  async onStart(ctx) {
    const va = ctx.airline.createClient();

    // Same axios API plugins use in the renderer via useVaApi():
    const { data } = await va.get<{ pilotID: string }>("/pilot/verify");
    ctx.logger.info("MyPlugin", `Verified ${data.pilotID}`);

    await va.post("/pireps", {
      flight_number: "QFA1",
      arrival: "EGLL",
    });
  },

  async onStop() {
    // axios instances don't need explicit teardown
  },
});

ctx.databasePluginDatabaseAccessor

Open (or create) an SQLite database file scoped to your plugin’s data directory at {userData}/plugins-data/{pluginId}/. Returns a better-sqlite3 Database instance:
const db = ctx.database.open("landing-reports.db");

db.exec(`
  CREATE TABLE IF NOT EXISTS reports (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    flight_id TEXT NOT NULL,
    landing_rate REAL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

const insert = db.prepare(
  "INSERT INTO reports (flight_id, landing_rate) VALUES (?, ?)"
);
insert.run("VA123", -180.5);

const rows = db.prepare("SELECT * FROM reports").all();
ctx.logger.info("DB", `Found ${rows.length} landing reports`);
The directory is created automatically on first use. Each plugin gets its own isolated directory — plugins cannot access each other’s databases.

ctx.serverPluginServerRegistrar

Register Express routers on the shell’s internal HTTP server:
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 mounted at /api/my-plugin/status — use STRATOS_APP_BASE from the SDK to build the full URL. 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 { createPlugin } from "@skyvexsoftware/stratos-sdk/helpers";
import { Router } from "express";

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 default createPlugin({
  async onStart(ctx) {
    ctx.server.registerRouter("/api/my-plugin", router);
    ctx.logger.info("MyPlugin", "Routes registered");
  },

  async onStop(ctx) {
    ctx.logger.info("MyPlugin", "Stopping...");
  },
});
In your UI module, query this endpoint with TanStack Query:
import { useQuery } from "@tanstack/react-query";
import { STRATOS_APP_BASE } from "@skyvexsoftware/stratos-sdk";

function FleetList() {
  const { data, isLoading } = useQuery({
    queryKey: ["fleet"],
    queryFn: () =>
      fetch(`${STRATOS_APP_BASE}/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>;
}

Communication Between Background and UI

For UI-to-background communication, the recommended pattern is Express routes + TanStack Query. Register routes in your background module with ctx.server.registerRouter() and query them from your UI module using TanStack Query, as shown in the Registering Express Routes section above. This approach composes naturally with React’s rendering model — loading states, error handling, caching, and refetching all come for free via TanStack Query. For background-to-UI push (data the UI didn’t ask for, like live updates or alerts), you can use ctx.ipc.send(). UI modules subscribe with socket.on from usePluginContext() — both ends use the leaf event name, the shell scopes the channel to your plugin id automatically.
// Background: push an update to every renderer of this plugin
ctx.ipc.send("status-update", { connected: true, pilots: 42 });
// UI: subscribe in a component
import { usePluginContext } from "@skyvexsoftware/stratos-sdk";
import { useEffect, useState } from "react";

export function StatusBadge() {
  const { socket } = usePluginContext();
  const [status, setStatus] = useState<{ connected: boolean; pilots: number }>();

  useEffect(() => {
    const handler = (payload: unknown) => setStatus(payload as never);
    socket.on("status-update", handler);
    return () => socket.off("status-update", handler);
  }, [socket]);

  return <span>{status?.connected ? "online" : "offline"}</span>;
}
A few rules:
  • Leaf names only. Don’t include plugin:{id}: in the channel — the shell prefixes both ends, doubling it would broadcast to nothing.
  • Fire-and-forget. A UI that mounts after a broadcast doesn’t see it. Pattern: HTTP fetch on mount, then subscribe for updates.
  • Same plugin only. Plugin A’s UI cannot subscribe to plugin B’s broadcasts.
  • No UI → background socket.emit. Use HTTP via ctx.server.registerRouter instead.
  • Structured-cloneable payloads. Functions, DOM nodes, and class instances with methods do not survive Electron IPC serialization.

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(ctx) for each running background module
  5. After an OTA update, the shell calls onStop(ctx) then onStart(ctx) fresh