Docs

Plugin SDK

Most plugins are pages in a locked down frame: themes, file viewers and small apps. Some need more: their own tables, pages in the Stavo shell, background jobs, live connections. Those are server-side plugins(runtime module@1), and they use the Plugin SDK.

Trust first

  • A server-side plugin runs inside your Stavo server. So only packages signed by the official catalog install and run.
  • The manifest lists the capabilities it uses. Admins see them before installing, and the SDK refuses anything that wasn't asked for: a plugin that tries is stopped before it starts.
  • Capabilities are a promise the catalog checks, not a sandbox: the signature is what you're trusting.
  • To build your own, the server's operator can set STAVO_TRUSTED_SIDELOAD=1 to run unsigned builds (from a zip or the STAVO_PLUGINS_DIR folder). Don't on a server other people use.

The manifest

json
{
  "manifest": 1, "id": "hello", "name": "Hello", "version": "1.0.0", "publisher": "You",
  "summary": "Says hello.", "icon": "icon.svg",
  "runtime": "module@1", "entry": "server/index.js",
  "compat": { "stavo": ">=0.1.0 <2.0.0", "sdk": "^1.0.0" },
  "network": ["api.example.com"],
  "module": {
    "capabilities": ["ui", "features", "db", "routes", "net", "cli"],
    "migrations": "migrations",
    "cli": [{ "name": "hello", "summary": "Say hello", "usage": ["", "<name>"] }]
  }
}
CapabilityWhat it allows
uiAn app in the app switcher with its one sidebar, an Admin section, a Settings link, "Open with" actions in the Drive, pages in the shell
featuresSwitches in Admin > Features (Everyone or Only selected people); the first one owns the plugin's pages and APIs
limitsPer-person limits admins set on each person's page, with defaults
dbIts own tables, in its own schema, from its migrations folder
settings, secretsSettings for the server and per person; secrets sealed with the server's key
driveRead files people pick, save files to their Drive, make share links (quota and permissions checked)
notifications, activityThe bell (and email), the activity log
jobsBackground work on a timer, stopped when the plugin is switched off
routes, public_routesAPIs under /api/v1/x/<id> and pages under /dashboard/x/<id> (signed in), public pages and webhooks under /x/<id>
socketsWebSockets from its pages
cli, toolsstavo <command> in the CLI; tools for the Assistant and MCP clients
net, lanHTTPS to the hosts in network (public addresses only); machines on your network an admin added

The code

The entry exports activate(host). Everything it does with Stavo goes through host. Write it in TypeScript with JSX (hono/jsx), and bundle it with hono and @stavo/plugin-sdkleft out: the server provides them, so your pages render inside its shell.

tsx
import { Hono } from "hono";
import { definePlugin } from "@stavo/plugin-sdk";

export default definePlugin({
  activate(host) {
    host.features.register({ id: "hello", name: "Hello", description: "Says hello.", hides: "the Hello app", perPerson: true });
    const { SideSection, SideLink } = host.ui.components;
    host.ui.app({ label: "Hello", icon: "sparkle", feature: "hello",
      sidebar: (ctx) => <SideSection label="Hello"><SideLink href="/dashboard/x/hello" label="Home" active /></SideSection> });

    const pages = new Hono();
    pages.get("/", (c) => host.ui.page(c, { title: "Hello", children: <h1 class="page-title">Hi {host.auth.user(c)!.display_name}</h1> }));
    host.routes.pages(pages);

    const api = new Hono();
    api.get("/count", async (c) => c.json((await host.db.sql`SELECT count(*)::int AS n FROM greetings`)[0]));
    host.routes.api(api);

    host.cli.handle(async (user, command, args) => ({ text: `hello ${args[0] ?? user.username}` }));
  },
});

What host gives you

  • host.ui: app, adminSection, settingsLink, driveAction, page and adminPage (a page in the shell), asset (a URL for a file in your package), and the shell's Select, Icon, SideSection, SideLink components.
  • host.features: register, on, allowed(user, id). host.limits: define, get(userId), set, defaults.
  • host.db.sql: a postgres.js client with your schema first on the search path. Your migrations ran before activate.
  • host.settings, host.secrets, host.drive (file, read, folder, list, save, shareLink), host.notify, host.activity (record, label).
  • host.jobs.every, host.routes, host.auth (user(c), requireAdmin, viaToken), host.sockets (handle, upgrade), host.people, host.net, host.cli, host.tools.

The full, typed list is plugins/sdk/index.ts in the server repository (the @stavo/plugin-sdk package).

Lifecycle

  • Install or update: the package is checked (signature, hash, compatibility), its migrations run, then activate.
  • Switch off (or turn off Plugins in Admin > Features): everything it registered goes away, its jobs stop, its data stays.
  • Uninstall: the admin chooses to keep its data (it picks up again if reinstalled) or delete its tables, settings and limits for good. Files it saved in people's Drives stay.

Build and publish

shell
bun plugins/build.ts hello                 # in the server repository: bundles server/index.js and the browser assets
bun scripts/publish-plugin.ts ../server/plugins/hello/dist   # in the product site repository: zips, hashes, signs

Official plugins are published by NullDaily through the catalog. Versions only go up.