Skip to main content

By the end of this tutorial you’ll have a NaaP plugin running locally inside the operator.livepeer.org shell, a backend API that queries a Postgres schema isolated to your plugin, and a plugin.json manifest ready to publish to the marketplace. The plugin uses ShellContext for auth, navigation, and theme, which means you write product code without rebuilding the infrastructure underneath it. This is the Persona 2 and Persona 3 join: the platform builder shipping operator tooling and the compute primitive builder shipping a network utility. NaaP is where both audiences ship UI that reaches the Livepeer operator community directly.
NaaP is an official Livepeer project in active beta. Breaking changes to the plugin SDK may occur between releases. Check the changelog at operator.livepeer.org/docs/community/changelog before upgrading.

Required Tools

  • Node.js 20 or later
  • Docker (for the local PostgreSQL container)
  • Git
  • A code editor
The full platform runs locally via ./bin/start.sh in the cloned NaaP repository. First run installs dependencies, starts Postgres, runs schema migrations, builds plugin bundles, and starts the shell. Subsequent starts take 6-8 seconds.

Plugin Architecture

NaaP is a micro-frontend platform. The shell is a Next.js 15 host application at operator.livepeer.org. Plugins are compiled to UMD bundles that the shell loads at runtime via a plugin registry. Each plugin owns a full vertical slice. Every plugin receives a ShellContext object on mount. This is the entire interface between a plugin and the platform. Plugins do not import shell internals directly.
Plugin backends run at /api/v1/[plugin-name]/* in production (Vercel) and proxy to standalone Express backends on ports 4001-4012 in local development. The same route handlers serve both environments.

Platform Bootstrap

1

Clone the platform

2

Start the platform

First run takes 5-10 minutes. The script installs dependencies, starts Postgres via Docker, runs schema migrations, generates .env files, builds plugin bundles, and starts the shell.Subsequent runs take 6-8 seconds.
3

Open the shell

Navigate to http://localhost:3000. The NaaP shell loads with the default plugins installed. Sign in with the development credentials displayed in the terminal output.The 12 default plugins cover developer, operator, monitoring, and governance use cases. Your custom plugin will mount alongside them after scaffolding.

Plugin Scaffold

1

Scaffold a new plugin

The CLI scaffolds a complete plugin skeleton: React entry point, backend route handlers, plugin manifest, Postgres schema migration, and test harness.
2

Edit the manifest

Open plugin.json:
The five required fields are id, name, version, category, and permissions. The shell uses id for routing (/orchestrator-health), database namespacing (orchestrator_health schema), and API path (/api/v1/orchestrator-health/*).
3

Start the dev server

The plugin hot-reloads inside the shell at http://localhost:3000/orchestrator-health. Edit React components and see changes within a second.

Frontend Implementation

The plugin entry point receives ShellContext via the useShell() hook. Save as src/index.tsx:
The plugin imports only @naap/plugin-sdk and pulls every shell service through hooks. useShell() returns navigation, notifications, and logger; useAuth() returns the authenticated user. No direct calls to shell internals.

Backend Routes

Plugin backends run as route handlers under /api/v1/[plugin-name]/*. The standard response envelope wraps every response. Save as src/api/orchestrators.ts:
The @naap/database module exposes the shared Postgres connection. Plugin schemas are isolated by Postgres schema name; the Orchestrator-health plugin owns the orchestrator_health schema and cannot read or write to other plugins’ schemas. Wire the route handler in src/api/index.ts:
The shell mounts this router at /api/v1/orchestrator-health/. Every request gets pre-authenticated by the shell middleware; the route handler trusts that the request has a valid user.

Database Schema

Plugins ship migrations under migrations/. The CLI generates timestamped files; the migration runs on shell startup and is idempotent. Save as migrations/001_init.sql:
Schema isolation is enforced at the Postgres level. Other plugins’ role grants don’t include the orchestrator_health schema, so cross-plugin reads fail with permission denied even if a plugin tries to escape its boundary.

Inter-Plugin Communication

Plugins communicate through the shell’s event bus. Use it to publish notable state changes (a new Orchestrator detected, an alert threshold crossed) without coupling plugins to each other.
Event types follow a [domain].[verb] convention. The shell broadcasts events for plugin lifecycle (plugin.installed, plugin.removed), auth (user.signed-in, user.signed-out), and theme (theme.changed). Custom events use your plugin’s namespace (orchestrator-health.alert).

Marketplace Publication

NaaP ships a Plugin Publisher plugin (one of the 12 default plugins) that handles marketplace submission. The flow:
1

Validate the bundle

The validator checks the manifest, schema migrations, permission declarations, and bundle size. Errors block publication; warnings allow but flag for review.
2

Build the production bundle

Output goes to dist/. The UMD bundle is what the shell loads at runtime.
3

Publish via the Plugin Publisher plugin

Open the Plugin Publisher plugin in the live shell at operator.livepeer.org/plugin-publisher. Upload the bundle plus the plugin.json manifest. The publisher walks the same validation, then submits to the marketplace queue for review.
4

Marketplace review

Plugins go through a review process before listing. Reviews check for security issues (escalating permissions, cross-plugin schema access, unsafe network calls) and UX consistency. Approved plugins appear in the Plugin Marketplace plugin for any operator to install.

Production Considerations

Six things change between the local development flow and a published plugin. Permission scoping. Declare only the permissions your plugin actually uses. Marketplace review rejects overscoped manifests. If the plugin reads Orchestrator data, request orchestrator:read, not network:write. Schema migrations are forward-only. Once a migration runs in production, you cannot undo it. Test migrations against a fresh database before publishing; never edit a published migration file. Backend cold-start. Vercel functions cold-start on first request after idle. For plugins that need consistent latency, batch initial loads or use the shell’s caching layer. AI prompt templates. The Prompts section in the NaaP docs ships eight templates for plugin scaffolding, UI design, testing, and publishing. Paste them into any AI assistant for boilerplate generation. Versioning. Use semantic versioning for the manifest version field. Breaking changes (manifest schema shifts, permission additions) increment major. Bug fixes increment patch. Changelog discipline. The NaaP platform itself ships breaking SDK changes between beta releases. Subscribe to the changelog at operator.livepeer.org/docs/community/changelog and test against the next beta before it ships stable.

Common Errors

The plugin.json is missing a required field or has an invalid value. Run naap-plugin validate for specific errors. The five required fields are id, name, version, category, permissions.
The Express backend is running on the right port (4001-4012) but the shell isn’t proxying to it. Restart the shell with ./bin/start.sh --restart-shell to pick up the new plugin’s route handlers.
The plugin schema didn’t exist before the route handler ran. Confirm migrations/001_init.sql ran during ./bin/start.sh; the schema name must match plugin.json’s id with hyphens converted to underscores (orchestrator-health becomes orchestrator_health).
The plugin is being rendered outside the shell. Either you’re loading it directly (not through naap-plugin dev), or a parent component caught the React tree and broke context propagation. The plugin only works inside the shell host.
useEventBus() returns a new subscribe function on every render unless wrapped in useCallback. The subscriber closure captures stale state. Pass the values you need to the publisher; don’t rely on closure over render-time state.
Your plugin is registered in the NaaP Shell and accessible from the portal. Publish it to the plugin registry to make it available to other NaaP operators.

AI agent prompt

Next Steps

NaaP Docs

Full developer documentation, SDK hooks reference, prompt templates.

NaaP Repo

Source code, contribution guide, issue tracker.

Pymthouse Tutorial

Pair a NaaP plugin with pymthouse for billed multi-tenant access.

Eliza Plugin Tutorial

Build an AI agent plugin for the Eliza framework.
Last modified on May 31, 2026