Plugin System
How Aviato plugins extend libraries, the ingestion pipeline, and the UI.
Aviato is a thin core wrapped around a plugin runtime. Almost
everything that makes a media library useful (discovering files on
disk, identifying them against TMDb or MusicBrainz, generating
thumbnails, parsing .nfo sidecars, painting an item-detail page)
ships as a plugin. Aviato orchestrates work; plugins do it.
This page is the entry point. For deeper topics see:
- Library capability, for declaring
mediaTypes, item schemas, entities, and renderer slots. - Hooks, events, and views, for the full pipeline, event, and UI-extension catalog.
Anatomy of a plugin
Every plugin is a directory with at minimum:
my-plugin/
├── plugin.json # manifest (the only file Aviato reads to discover the plugin)
├── package.json # standard Bun/Node package metadata
└── src/index.ts # entry point that calls createPlugin({...})Plugins are loaded from the data directory at <data-dir>/plugins/.
Each subdirectory with a valid plugin.json becomes a plugin.
Runtime model
Plugins run as separate processes, not in-process modules. This is deliberate: it isolates crashes, lets plugins use any runtime, and prevents a misbehaving plugin from blocking Aviato's event loop.
| Aspect | Detail |
|---|---|
| Transport | JSON-RPC 2.0 over stdio |
| Engines | bun (default), node, python, binary, declared in the manifest's engine field |
| Process model | One process per plugin, regardless of how many capabilities it provides |
| Discovery | Aviato walks the plugin directory at startup, reads each plugin.json, and validates the manifest |
| Lifecycle | discovered → starting → running → stopping → stopped, with error as a sticky terminal state |
Plugin authors interact with Aviato exclusively through the
@aviato/plugin-sdk
npm package.
What a plugin can do
A plugin exposes its functionality through two layers of the manifest.
Capabilities (capabilities: string[]) are long-lived,
RPC-driven contracts Aviato calls into when it needs something done.
Each capability is a named bundle of methods. Current set:
| Capability | What it does | Method namespace |
|---|---|---|
filesystem | Discovers, reads, watches, and (optionally) writes files | filesystem.* |
indexer | Identifies files against an external metadata source like TMDb or MusicBrainz | indexer.* |
library | Defines a library type: its schema, entities, browse rules, and renderer | library.* |
A single plugin can declare any combination of capabilities.
library-tv is purely a library plugin; aviato-tmdb is purely
an indexer; fs-local is a filesystem plugin. Nothing prevents
a plugin from declaring ["filesystem", "indexer", "library"] and
serving all three roles from one process.
Transcoding lives in Aviato's streaming pipeline, not in a plugin
capability. There is no transcoder capability.
Subscriptions (subscriptions: { events, hooks, views }) are
short-lived reactions to things happening in Aviato. A plugin with
no capabilities at all is still useful if it subscribes to hooks.
Most "auxiliary" plugins work this way: embedded-metadata,
external-subtitles, posters, and thumbnails declare
capabilities: [] and exist solely to mutate bundles during the
probe phase.
See hooks.mdx for the full catalog.
Manifest essentials
{
"id": "aviato-my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "What it does",
"author": "Aviato",
"license": "MIT",
"engine": "bun",
"entry": "src/index.ts",
"aviato": { "minVersion": "0.1.0" },
"capabilities": ["library"],
"mediaTypes": ["movies"],
"configuration": [
{ "key": "apiKey", "label": "API Key", "input": "text", "required": false }
],
"capabilityConfig": {
"library": { "bundling": { "strategy": "per-folder" }, "paths": [...] }
},
"subscriptions": {
"events": ["library.item.updated"],
"hooks": [{ "name": "pipeline.probe.afterProcess", "order": 40 }],
"views": ["ui.item.detail.actions"]
}
}Required fields: id (lowercase-kebab), name, version (semver),
description, author, license, engine, entry,
aviato.minVersion, capabilities (may be empty), and
capabilityConfig for any capability that declares one.
mediaTypes accepts two forms:
- Array form, like
["movies", "tv"]: a simple list. - Object form, like
{ "tv": { "episodic": true } }: lets a plugin attach per-mediaType config flags (episodic,seekable,idleProtection) consumed by the player and the host UI.
Aviato refuses to start any plugin whose manifest fails schema validation and surfaces the Zod error path in the admin UI.
Author surface (@aviato/plugin-sdk)
A single entry point, createPlugin, wires up everything.
import { createPlugin } from '@aviato/plugin-sdk'
const plugin = createPlugin({
// capability handlers (only the ones declared in the manifest)
library: { getSchema, getSortOptions, getFilterOptions, getGroupingOptions, getItemSummary, getItemDetail },
filesystem: { validate, scan, watch?, unwatch? },
indexer: { supports, index, search, getMatchDetail },
})
// subscriptions
plugin.events.on('library.item.updated', payload => { /* fire-and-forget */ })
plugin.hooks.on('pipeline.probe.afterProcess', async ({ itemId, bundle }) => {
return { /* BundleDelta */ }
})
plugin.views.on('ui.item.detail.actions', async ({ itemId, libraryType }) => {
return [{ type: 'action', id: '...', slot: 'detail-actions', label: '...', rpcMethod: '...' }]
})Capability handlers are request/response: Aviato calls them through JSON-RPC when it needs work done. Subscription handlers are reactive: Aviato pushes notifications and the plugin reacts.
The SDK is standalone, has no closed-source dependencies, and ships all of its types as Zod schemas. It can be vendored, audited, or extended without coupling to Aviato itself.
Working with plugins locally
# Plugin logs surface in /admin/plugins/<id>/logsPlugin processes are crash-recovered with exponential backoff. After a configurable number of consecutive crashes Aviato trips a circuit breaker on that plugin and surfaces an error state in the admin UI.
See also
- Library capability and schemas
- Hooks, events, and views
aviato-library-movies(a fulllibrarycapability) andaviato-embedded-metadata(a pure hook subscriber) are good reference implementations to read alongside these docs.