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=1to run unsigned builds (from a zip or theSTAVO_PLUGINS_DIRfolder). Don't on a server other people use.
The manifest
{
"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>"] }]
}
}| Capability | What it allows |
|---|---|
ui | An 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 |
features | Switches in Admin > Features (Everyone or Only selected people); the first one owns the plugin's pages and APIs |
limits | Per-person limits admins set on each person's page, with defaults |
db | Its own tables, in its own schema, from its migrations folder |
settings, secrets | Settings for the server and per person; secrets sealed with the server's key |
drive | Read files people pick, save files to their Drive, make share links (quota and permissions checked) |
notifications, activity | The bell (and email), the activity log |
jobs | Background work on a timer, stopped when the plugin is switched off |
routes, public_routes | APIs under /api/v1/x/<id> and pages under /dashboard/x/<id> (signed in), public pages and webhooks under /x/<id> |
sockets | WebSockets from its pages |
cli, tools | stavo <command> in the CLI; tools for the Assistant and MCP clients |
net, lan | HTTPS 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.
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,pageandadminPage(a page in the shell),asset(a URL for a file in your package), and the shell'sSelect,Icon,SideSection,SideLinkcomponents.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 beforeactivate.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
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, signsOfficial plugins are published by NullDaily through the catalog. Versions only go up.