LivinityDevelopers
Plugin SDK · v37.0

Extend LivOS with signed plugins.

A LivOS plugin ships routes, widgets, slash commands and MCP servers as a single signed bundle. The runtime mounts plugins at <user>.livinity.io/p/<plugin-id>/ with no server restart on install. Operator-signed for v37; community submissions open in v38.

01 · what a plugin is

One bundle. Four hooks.

A plugin is a .livpkg.tgz archive that LivOS extracts into /opt/livos/plugins/<id>/. The runtime reads the manifest, verifies the Ed25519 signature, and mounts whichever of the four hook types the plugin declares.

Routes

HTTP endpoints

Mounted under /p/<id>/ on the user's livinityd. Standard Express handlers — request, response, async middleware. Useful for webhook receivers, reverse proxies, and custom APIs that need to live on the user's own domain.

Widgets

UI components

Pre-compiled UMD bundles rendered into mount points across the LivOS shell — dock, settings panel, AI chat, window titlebar. Shadow-DOM by default for CSS isolation.

Commands

AI chat slash commands

Plugin-declared /foo commands appear in the user's Liv chat. The runtime routes args + session context to the plugin's handler and injects the response back into the conversation as a tool result.

MCPs

MCP servers

Register MCP servers (stdio or streamableHttp transport) into the user's mcpConfigManager. Same shape AI Chat uses today — your plugin's tools become available to the local Liv assistant on install.

02 · package layout

What goes inside a .livpkg.tgz

Everything the runtime needs to mount, sign and load the plugin lives in the archive. No external resources are fetched at install time except the bundle URL itself.

hello-world.livpkg.tgz ├── plugin-manifest.json # this spec ├── plugin-manifest.sig # Ed25519 detached signature over plugin-manifest.json ├── backend/ │ └── index.mjs # default export = PluginBackendModule ├── ui/ │ ├── bundle.umd.js # pre-compiled UMD, mounted via plugin loader │ └── styles.css # optional, Shadow-DOM scoped by default ├── migrations/ │ └── 0001_init.sql # optional, run by livinityd at install └── assets/ └── icon.svg # optional, served at /p/<id>/_assets/icon.svg
03 · manifest

plugin-manifest.json

One JSON file declares the plugin's identity, signing tier, every hook it exposes, and every capability it needs. The runtime refuses install if a capability is requested beyond the plugin's signing tier.

{ "manifestVersion": "1.0.0", "id": "hello-world", "version": "1.0.0", "name": "Hello World", "tagline": "Reference plugin for the LivOS SDK", "author": "Livinity", "signing": { "tier": "operator", "publicKeyId": "operator-v1", "signedAt": "2026-05-18T00:00:00Z" }, "hooks": { "routes": [ { "path": "/ping", "method": "GET", "handler": "pingHandler" } ], "widgets": [ { "mount": "dock", "component": "DockWidget" } ], "commands": [ { "slash": "/hello", "handler": "helloCommand", "description": "Say hello" } ] }, "capabilities": { "redis": [{ "keyPattern": "liv:plugin:hello-world:*", "access": "readwrite" }] }, "minLivosVersion": "37.0.0" }
manifestVersion: 1.0.0Ed25519 signatureredis capability scopespostgres capability scopesfilesystem capability scopesnetwork capability declared
04 · backend module

What backend/index.mjs exports

A single default export — a PluginBackendModule with activate / deactivate lifecycle hooks plus the named handlers referenced from the manifest.

export default { async onActivate(api) { api.log.info('hello-world activated'); // api.redis / api.pg / api.fs are namespaced + cap-checked await api.redis.set('liv:plugin:hello-world:greeting', 'hi'); }, async onDeactivate(api) { api.log.info('hello-world deactivated'); }, handlers: { // referenced by manifest.hooks.routes[].handler pingHandler(req, res) { res.json({ ok: true, at: '/p/hello-world/ping' }); }, }, commands: { // referenced by manifest.hooks.commands[].handler async helloCommand(args, ctx) { return `hi @${ctx.userId} — you said: "${args}"`; }, }, };
05 · ui bundle

UMD bundle, Shadow-DOM isolated

Plugins expose React components via a UMD wrapper. The plugin loader injects each declared widget into its mount point usingReact.createPortal wrapped in a Shadow-DOM root, so plugin CSS never leaks into the LivOS shell and vice versa.

// ui/bundle.umd.js (function (factory) { /* UMD prelude */ })(function () { return { // names match manifest.hooks.widgets[].component DockWidget(props) { return React.createElement('div', { style: { padding: 8 }, }, 'Hello, dock'); }, }; });
06 · publishing

Submission flow

v37 ships operator-signed-only. Community submissions open in v38 via this repo. For now, contact the operator and they'll co-sign and publish under operator-v1.

01

Fork utopusc/livinity-apps

The plugin repository hosts release artifacts and the public signing-key registry at .signing/pubkeys.json.

02

Build your .livpkg.tgz

Pack plugin-manifest.json, backend/,ui/, migrations/ and assets/ into a tarball. Run npm pack after structuring your directory.

03

Request co-sign

Open a pull request adding the bundle hash + manifest toplugins/<your-id>/. Operator reviews, signs plugin-manifest.sig with the operator key, and publishes a release.

04

Catalog entry

The operator adds a row to the Supabase apps table with section='plugin', pointing at the GitHub release URL + bundle SHA-256. Once committed it's live in the /store within seconds.

07 · reference plugins

Production examples

hello-world

Reference

The minimal plugin that touches every hook type — route + dock widget + slash command + redis capability. Use this as your starting fork.

livinity-broker

The first plugin

Claude API at /p/livinity-broker/v1/* via the user's Anthropic subscription. Demonstrates path-mounted routes and a plugin-managed settings widget.

SDK types

@livinity/plugin-sdk

TypeScript types for the manifest schema and the runtime API your backend module receives. Drop into devDependencies to get autocomplete for free.