Skip to main content
A plugin’s background and UI modules run in separate processes. Two patterns connect them: HTTP (Express routes + TanStack Query) for request/response, and IPC push for background-to-UI broadcasts.

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.