For developers
Build a plugin
A plugin adds something to somebody's media server: a metadata source for a collection nothing else identifies, extra detail on a film's page, a service we have not thought of. This is how to write one, package it, and get it listed.
The model, in one page
A plugin is a WebAssembly module that the server loads into its own process and calls. It has no filesystem, no ambient network, no way to browse the library, and no way to reach another plugin. It gets exactly the powers its manifest asks for and the owner approves, and nothing else. Even the one grant that touches the library only answers "do you have these ids", never "what do you have".
The way that is enforced is worth understanding early, because it changes how you debug: a permission is not a check, it is whether the function exists. A capability is not a permission check, it is whether the function exists. Ungranted host functions are never added to your module, so calling one fails at load with a link error rather than at runtime with a refusal. There is no permission-check path for the host to forget.
Plugin code never runs on a client. A TV has no browser engine to run it in, and per-client plugin code is the thing that killed the last generation of media-server plugins. You return data; the web app, the phone apps and the TV apps render it natively. That is why one plugin works on all seven surfaces without you writing anything for any of them.
The contract is frozen-additive within an API version. Frozen-additive within an apiVersion. New families, ops, block types and manifest fields may appear; existing ones will not change meaning or disappear. A breaking change would ship as apiVersion 2, and a server refuses a package whose apiVersion it does not speak, so an old plugin never half-works.
Pick what you are building
A plugin declares one or more families. A family is a job the server has and will hand to you.
Available now
- Matching services
matching - Be a library's metadata source. The owner picks your plugin per library, and from then on the scanner, the fix-match dialog, artwork picking and every background refresh ask you instead of the built-in provider.
- Fields and detail sections
fields - Put your own information on a detail page, on all seven surfaces. Return plain fields and they become a fact list for free; return blocks when you want more control.
- Search hooks
search - Contribute an attributed section to search results, under the library's own. A slow plugin is dropped from that query rather than allowed to hold up results.
- Event consumers
events - Be told what someone watched, as it happens. A scrobbler is the shape this exists for: start, pause, resume and stop, with a progress percentage, so a service can keep a watch history in step rather than guessing from a single write at the end.
- Subtitle providers
subtitles - Offer subtitles for a title and deliver the one the owner picks. Your track then sits in the subtitle list on every screen, indistinguishable from one that came with the file.
- Skip-segment providers
segments - Supply the intro, recap, preview, credits and commercial markers behind the Skip button. The owner orders providers when more than one has an opinion.
- Theme packs
theme - Ship a look. Your themes appear in everyone's theme picker beside the built-in ones, on every screen.
- Plugin pages
pages - Whole pages of your own, built from the same blocks plus container blocks whose ids the server turns into ordinary item cards, so your page renders library-native cells on every surface. Reached from a link in a detail section.
- Plugin libraries
source - Be a library whose items are not files on disk. You supply a catalog and the server turns it into ordinary movies, episodes and books: they browse, search, sort, resume, count as watched, go in playlists, and sit under the household rating cap, because they are the same rows every screen already draws. Planned and ruled in D176; podcasts and audiobooks joined in D184.
- Channels
channel - Several libraries behind one card, with a page of your own. A channel is one entry on the household's Channels shelf: open it and you get the page you shipped, made of your libraries. Its libraries are ordinary plugin libraries (they sync and play through the source ops, so a channel declares the source family too), with one difference: they do not appear on the home page or in the library list, because the channel is where people browse them. Everything else still finds them, so a title of yours turns up in search, in Keep Watching, in a playlist and in the queue, under the household rating cap like anything else. Ruled in D177.
On the roadmap
Declared in the contract and not yet callable. Listed so you can see where this is going, and so nobody builds a plugin against a family that does not answer yet.
Drawing something we did not think of
Most of the display vocabulary is a shape we chose for you: a fact list, a badge row,
a shelf of cards. tiles is the one
that is not. It is slots you fill, so a row nobody anticipated needs no new block
type, no server release and no update to any app.
A tile carries art, a title, a subtitle, a badge, a state mark, progress and a link.
The block then says two separate things: layout is how the tiles are
arranged, and shape is what one tile is. They are separate so every
arrangement can draw every card. A layout is a row that scrolls sideways, a list read
down the page, a grid that wraps, or a hero that fills the width. A shape is the cast
circle, the 16:9 shelf card, the 2:3 poster or a square. All four are cards the app
already draws everywhere else, which is the point of keeping the set closed: you pick
a card the viewer has seen a thousand times, so your content looks like the app rather
than like a plugin.
For the art you name a person or a title by id and the server resolves the picture for whoever is looking, or you give a monogram and a generic symbol and the server draws it. You always give the monogram: a face you asked for may not be one this library holds, and the disc is what keeps the row whole. A tile whose art names a title by id also gets that viewer's own progress and watched tick written over whatever you sent, so you can never draw a wrong tick on something the host knows about.
A hero is the home banner's shape: one card across the width, dots paging
through the rest. It is the only place a tile carries a description line, an eyebrow
and up to two buttons, and the only one that moves on its own. Set rotateSeconds and it advances, pausing while somebody points at it or has
it focused. Buttons stop at the hero on purpose: a card with buttons inside a scrolling
row is a thing nobody can aim at with a remote.
Three small blocks cover what pages used to fake with fact lists. stats draws a figure large with
its caption under it, for the number that is the point of your section. progress draws labelled bars and lets you write the figure at the end
yourself, so "6 of 10" reads the way you meant it. notice is one toned line of state with at most one button: "Connected as
kelly" and a Disconnect. A second response would make it a menu, and links already draws menus.
columns is for a page that wants
two things beside each other: two or three columns of ordinary blocks, a fact list
next to a set of bars. Nesting stops at one level. On a phone and on both TV skins the
columns stack in order, because a D-pad crossing columns of different heights is its
own kind of broken. It is the one block an older app cannot skip safely, so the server
hands that app the blocks inside it instead, in column order, and you do not have to
think about it.
The awards example is built on this. It decides that the awarding body is
the headline, the category is the second line, a win gets a check and a nomination an
open ring. None of that is in the contract. Nothing anywhere in the server knows what
an award is, which is exactly why your own row can look nothing like that one. The showcase example draws every shape, layout and widget on one page, with
the source next to each one: a rotating hero with buttons, the figures, the bars beside
a fact list in a two-column split, and the same tiles as posters, wide cards, squares
and circles.
Be a podcast or an audiobook library
A source can be audio. Declare shape: "podcast" and your series are shows and your items their episodes;
declare shape: "audiobook" and your items are books. They land as the same
shows, episodes and books a folder or a feed makes, so they get the audio player, Keep
Listening, bookmarks, speed memory, offline downloads and the rest without anything on a
screen knowing where they came from. A channel's libraries may take either shape too.
Items gain the audio fields: authors, narrators, the series a book belongs to and its place in it, ISBN and ASIN for a book; a guid, a published date, a transcript and explicit for an episode; chapters and a square cover for both. The reference lists every one with its limits. A show gains its author, categories and language.
Say where the file is, and you can skip resolve. Give an episode or a
single-file book its enclosureUrl, or a book in several files its parts in order, and the server plays those addresses without calling source.resolve. They are held to your http grant and to https,
exactly as a resolved address is. A book in thirty files is one book to the listener:
one length, one position, and the server stitches the files. Send each part's durationS if you know it, because the first play of a long book places the
parts it has not measured yet by what you declared. Leave the addresses out when they
are signed or expire, and implement source.resolve instead.
Name the release. A plugin with an audio library sets minHostVersion to 0.74.0
or later, or it will not install. An older server does not know the shapes, and the
version is how it says so.
Episodes somebody touched stay. In a podcast library, a complete sync that stops listing an episode removes it only if nobody played, bookmarked, downloaded, rated or saved it anywhere. An audiobook library prunes like any other library.
examples/plugins/librivox is the worked audiobook example: a LibriVox
book is its single M4B where the volunteers made one, else its section mp3s as parts,
with a chapter per section. examples/plugins/archiveaudio is the podcast
one: old-time radio sets from the Internet Archive, each a show, every mp3 in it an
episode dated the day it aired. Both declare their files, so neither implements
resolve. Both keep their mapping in plain Go files that go test runs
against recorded answers, with the wasm glue alone in main.go. And both
show the grant rule at work: LibriVox publishes its files on www.archive.org, a different host to a grant than archive.org, so the plugin rewrites every address to the one host it
asked for.
Be a channel
A channel is a place rather than a page. It is one card in a Channels row on the household's home screen, and behind that card a page of your own, made out of libraries you supply. Reach for it when what you are building is a whole corner of somebody's server: several libraries that belong together, and a front door for them.
You declare it once, in a channel block: the name and the line under it on
the card, a 16:9 picture from inside your own package, whether the channel is the
household's or belongs to each person who connects an account, up to ten libraries, and
the page they live on. Each library is an ordinary source declaration plus a key and a name. They sync and play through the source ops, so
a channel declares the source family as well as channel, and
the only thing that changes in code you have already written is that a sync and a
resolve now tell you which library they are about, in library.key. A
plugin with one library is never sent that field.
A channel's libraries stay out of the household's library list. The channel is where people browse them, and listing them again beside somebody's Movies folder would be the same titles in two places. Everything that reads items rather than libraries still finds them: search, Keep Watching, playlists and the play queue, all under the household's rating cap exactly as before.
You ship the page, and then the household owns it
This is the part that is different from every other family, so it is worth reading
twice. Your page is a layout: a list of rows you describe, which the server
draws out of things it already has. A hero fed from one of your libraries, a shelf from
one of your libraries, a heading, a collection shelf the owner adds. Four of the five
kinds are drawn by the host, which is why a channel page needs no new code on any of
the seven surfaces and why an owner rearranging it can never make it slow or break it.
The fifth kind is slot: your own row, whatever channel.render returns for that id, in the ordinary block vocabulary. A
slot is the one row an owner may hide but never add, because the host has nothing to
fill one with.
What you ship is the default. The owner then reorders it, hides rows they do not want, retunes a sort or a card shape, and adds host-drawn rows of their own. The server stores only their edits, not a copy of your page, and merges them over your current default every time the page is read. Three consequences, and each one is something an author will otherwise be surprised by:
- A row you add in an update appears at the bottom. It never pushes into the middle of a page somebody arranged, and nothing they placed moves.
- A row you remove takes its customisation with it. If they had retitled it or changed its sort, that edit goes when the row does, because it described a row that no longer exists.
- A row they retitled keeps their title. Anything they never touched follows your default forever, so you can retitle a row or change its sort in an update and it reaches everyone who left that row alone.
All three hang on one thing: the block id is the merge key. Keep ids stable across versions. Renaming one in an update is the same as deleting a row and adding a different one, and the owner loses whatever they had arranged on it without having touched anything.
examples/plugins/channeldemo is the worked example: two libraries, a
catalog it carries so it needs no network, a card picture, and a default layout with a
hero, two shelves, a heading and one slot. Copy it and change the catalog. Set minHostVersion to the release that first shipped channels, because a
server older than that does not know the family at all.
examples/plugins/archivechannel is the same thing against a service that
is really up: eight libraries of public-domain film and television from archive.org,
one of them show-shaped with real episodes. Read it for the two problems a real
catalog brings. It carries a list of titles it vouches for, each with a TMDB id, so
the host enriches those by id and they arrive with proper posters and cast; the
alternative was a grid of the archive's own 180x124 thumbnails. And that list is the
DEFAULT library rather than the top of it, because a public archive publishes no
content rating and its most-watched sorts are not what you want on a family server's
front page. Widening to the whole collection is a setting the owner turns on.
Bring your own colours
A channel may declare a theme inside its channel block: accent, focus, bg, a two-stop gradient, panel, text, dim, an
optional cardRadius, and an optional bgImage naming a file
inside your own package. There is no new vocabulary to learn, on purpose: this is the
same shape a theme pack ships and the same one the server has drawn since custom
themes existed, which is why every one of the seven surfaces drew channel colours on
the day the feature landed.
It follows your content rather than the navigation. Your colours are used on your channel's page, on the page of any title from one of your libraries however somebody reached it, and in the player while one of them is playing. Opening one of your films from a search result looks exactly like opening it from your own page, because what decides is where the title came from.
Rows of cards are never themed, and neither is the household's home screen. One Keep Watching row can hold titles from four different channels, and a row that changed colour card by card would be noise rather than identity. Your look starts when somebody is looking at one of your titles and stops when they leave it, and nothing has to put it back: it was never set anywhere else.
An unreadable theme does not install. The pairings that decide whether a screen can be used at all, body text against each surface it sits on, are checked when your package is read, and a theme that fails one is refused by name before anything is installed. Pairings that only cost polish, an accent that barely leaves your background, are your judgement and install fine. The line is drawn there because nobody chooses your colours the way an owner chooses their own.
Anyone can also decline the whole idea, with Always use my theme
beside their own, and the offline player always paints theirs. So treat a theme as a
finish and never as a layout: a channel that only makes sense in its own colours is
broken for the person who turned them off. Set minHostVersion to the
release that first shipped channel themes, because an older server drops the field
without a word and installs your channel with no colours at all.
Write one
Any language that compiles to WebAssembly works. Go needs no toolchain beyond Go itself, so that is what the examples use. Your module exports two functions and answers JSON:
//go:build wasip1
package main
import "encoding/json"
//go:wasmexport mp_alloc
func mpAlloc(size int32) int32 { /* reserve size bytes, return the offset */ }
//go:wasmexport mp_handle
func mpHandle(ptr, length int32) int64 {
var call struct {
Op string `json:"op"`
Input json.RawMessage `json:"input"`
}
json.Unmarshal(read(ptr, length), &call)
switch call.Op {
case "match.movie":
return reply(map[string]any{"data": findFilm(call.Input)})
}
return reply(map[string]any{"data": nil}) // null means "no match"
} Build it, and you have a plugin:
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o plugin.wasm . 19 worked examples exist, each one built and run by our own test suite on every change, so none of them can quietly rot. Start by copying whichever is closest to what you are building.
anilistmatching- Matches anime against AniList, which knows series and films that general movie databases handle poorly. Fills in episode titles too.
archivesource- Turns a collection at archive.org into a library of films your household can watch. Point it at a collection (Prelinger's ephemeral films, feature films, public-domain shorts) and its titles arrive as ordinary movies: they browse, sort, resume, go in playlists and sit under the same rating limit as everything else on the server. Your server fetches every stream itself, so nothing on a TV or a phone ever talks to archive.org.
archiveaudiosource- Radio drama from the 1940s and 1950s, from the Internet Archive, as a podcast library: Dragnet, Gunsmoke, X Minus One, Suspense and ten more, each a show with every surviving broadcast as an episode, dated the day it aired. The shows are a fixed list of sets the Old Time Radio Researchers assembled and the Archive keeps in its oldtimeradio collection; nothing else from the Archive is ever listed. Your server fetches every episode itself, so nothing on a phone or a TV ever talks to archive.org.
archivechannelsource, channel- A whole channel of public-domain film and television from archive.org, arranged rather than dumped: eight libraries (features, noir, science fiction and horror, comedy, silent film, animation, newsreels, and classic TV as real series with real episodes) behind one card, with a page that arrives laid out and belongs to your household afterwards. The titles on the front are a curated list this plugin carries, matched to TMDB by id, so they arrive with proper posters, backdrops and cast rather than the archive's own thumbnails. Your server fetches every stream itself, so nothing on a TV or a phone ever talks to archive.org.
awardsfields, pages- Awards and nominations from Wikidata. A film's page gains a row of award circles beside its cast: who gave it, what for, and whether it was won or only nominated. A person's page lists their awards and shows which of the winning titles are already in your library. Needs no account and no API key.
catalogdemosource, events, search, pages- A reference for the kind of plugin library that belongs to a person: connecting an account is what creates it, and disconnecting removes it. It also searches its own service and draws a page for a title you do not have yet, so the four families that make up a searchable, streamable catalog are all here in one package. Used by the test harness.
channeldemosource, channel- A whole channel with nothing behind it: two libraries (Films and Series) whose catalog is a table this plugin carries, a card on the Channels shelf, and a page whose default layout ships with the plugin and belongs to the household afterwards. It reaches nothing on the internet and needs nothing configured, so a harness and a curious owner get the same working channel. Its resolver has no media and says so in a sentence rather than handing back an address that would stall. minHostVersion is 0.65.0 because an older server does not know the channel family at all: keep that number equal to the release that carries channels, read from the published manifest at release time and never from a snapshot in the docs.
eventdemoevents- A reference event consumer with no network at all: it links, records what it is told, and can read its own log back. Used by the test harness.
feeddemosource, search- A reference plugin library: one channel and five uploads, produced as ordinary episodes so they browse, sort, resume and sit under the household rating cap like anything else, a resolver that hands the server a stream when somebody presses Play, and a search hook that sends a result straight to the item it already made. Its catalog is a table it carries, and it is pointed at its own media, so it needs nothing on the internet. Used by the test harness.
hello- The reference plugin: answers ping, echoes anything else it is given.
librivoxsource- Public-domain audiobooks read by LibriVox volunteers, as an audiobook library. Each book arrives with its authors, its readers, its cover and a chapter for every section, and plays as one book whether the volunteers made a single M4B of it or left it as a file per chapter. By default the library holds what LibriVox catalogued in the last three months and grows as they finish more; give it a genre to list that whole genre instead. Your server fetches every file itself, so nothing on a phone or a TV ever talks to LibriVox or the Internet Archive.
matchdemomatching, fields- A matching service that invents plausible metadata for anything. For trying out plugin matching without an internet connection.
pagedemofields, pages- The smallest complete plugin page, and the only example that asks for no permissions at all. It adds one link to every film and series, opening a page that shows exactly what the host handed the plugin. It is also the reference for per-profile settings and for both kinds of action button. Install it to see what a plugin page is made of; copy the awards example to build something real.
searchdemosearch, pages- A reference search hook with no network at all: it recognises a few film series by the names people actually type and offers their titles as an attributed section under your own results. Used by the test harness.
segdemosegments- A reference skip-marker provider with no network at all: it derives an intro, a recap and a credits marker from the item it is asked about. Used by the test harness.
showcasefields, pages- Every block shape and widget a plugin can draw, on one page, with the source next to it. Install it, open any film or series, and follow the link: you get a rotating hero, big figures, bars, a two-column split, and the same tiles drawn as posters, wide cards, squares and circles. It asks for no permissions at all. minHostVersion is 0.65.0 because an older server silently drops a block type it does not know: keep that number equal to the release that carries the D177 vocabulary, and raise it in the same commit as any newer shape.
subdemosubtitles- A reference subtitle provider with no network at all: it offers two pretend releases per language and hands back a short WebVTT naming the one you picked. Used by the test harness.
themedemotheme- Two looks and nothing else: a reference theme pack. It ships no code at all, which is the whole point of the family.
traktevents- Keeps your Trakt watch history in step with what you actually watch here. Each person connects their own Trakt account, and nobody else's viewing is ever sent.
If you only read one, read awards. It is the shape most real
plugins take: identify something by an id the server already has, fetch
a fact about it from somewhere public, cache the answer, and put it on a
detail page and a page of its own. If you are building a library,
read feeddemo (a catalog the whole house shares, with the
stream behind it and a search hook that opens the item it already made)
and then catalogdemo (the kind that belongs to one person,
created by them connecting their account). Both carry their own catalog
so they can be tested offline; archive is the same family
pointed at a service that is actually up, with the paging, the fields
that answer in whatever shape they like, and the download address that
redirects that come with one.
They live at github.com/kammcs/multipass-plugins, under a licence that lets you take any of them as a starting point.
Returning an error is not failing. A reply of
{"error": "..."} is your plugin working: it is logged and shown,
and it never counts toward the fault budget that auto-disables a broken plugin.
What counts is crashing, hanging, or refusing to load.
Describe it
manifest.json sits at the root of your package and is the document
that gets signed. manifest.json sits at the root of your package and is the signed document. It names every other file with its hash, so a signed manifest fully determines the package: an unlisted extra, a missing file, a size mismatch or a hash mismatch all refuse the package.
{
"id": "com.example.myplugin",
"name": "My Plugin",
"version": "1.0.0",
"apiVersion": 1,
"minHostVersion": "0.62.0",
"entry": "plugin.wasm",
"description": "One or two sentences an owner will read before installing.",
"author": "You",
"provides": ["matching"],
"capabilities": { "http": ["api.example.com"], "storage": true },
"settings": [
{ "key": "apiKey", "label": "API key", "type": "password",
"help": "From your account page at example.com." },
{ "key": "spoilers", "label": "Show spoilers", "type": "toggle",
"default": "off", "scope": "profile" }
],
"actions": [
{ "id": "test", "label": "Test connection" }
]
}
You do not write the files table. multipass plugins pack hashes
every file and writes it for you.
Name a minHostVersion when you use something new. apiVersion answers "is this the same contract", and it moves only when
something is removed, so it tells a server nothing about a block type or a slot that
arrived in a particular release. The vocabulary is additive, which means an older
server DROPS what it does not recognise: without this field your plugin installs
there, runs, answers correctly and draws nothing, and the owner blames you. With it,
that server refuses the install and names the version it needs. Leave it out if you
use nothing new.
Set yourself up
Installing your plugin does what installing it implies. If you are a channel the
household shares ("account": "none"), the server reads the libraries out
of your manifest and creates the ones it does not have, with your names and each
library's defaults, and starts their first sync. You write nothing for that: the
declaration is the instruction. Two shapes still go through the owner's add flow,
a per-profile channel, which is created when somebody presses Connect, and a plain source library, whose settings a manifest cannot guess.
If you have work of your own to do, declare "setup": true and answer plugin.setup. It runs once on an install, on every upgrade and on a
re-enable, with the capabilities you already declared and nothing more. reason says which of the three it is, and on an upgrade previousVersion names the version you replaced, which is your one moment
to migrate storage an older you wrote. Make it idempotent: a re-enable is often
somebody flipping a toggle for an unrelated reason. Return one sentence and the owner
reads it beside your plugin; return nothing at all and that is a success too.
Declaring setup and never writing the handler costs nobody an install, it is a line in
the log.
A failed setup leaves your plugin installed, says what went wrong, and offers to run it again. Nothing is rolled back and nothing is switched off, so a service that was down for a minute does not cost somebody a working plugin.
Anything slow, inside setup or anywhere else, should say so. progress()
takes a label, a fraction, a line of detail and done when you finish, and
draws one line in the owner's server activity. It needs no capability and returns
nothing. It is the difference between a plugin that is working and a plugin that looks
hung.
Settings, and whose they are
A setting is the household's unless you say otherwise: the owner fills it in
once, on your plugin's card. Mark one "scope": "profile" and it
becomes each person's own instead, set on their own settings screen, with your
default as the fallback for anyone who has not. The owner never
sets it on somebody's behalf.
Read them the way you always do, through config(). What comes back
is already merged for whoever the call is for, so nothing in your code asks
whose values these are, and nothing can.
One thing to know before you reach for it: a page from a plugin with a
per-profile setting is cached per person, so you are trading cache hits
for the choice. And item text from describe.item is stored once for
the whole house, so a per-profile setting cannot change it.
An action is a button the host draws beside your settings.
Pressing one calls settings.action with that id, and you return one
sentence. Not blocks: the settings screen is not a place to paint. Give it
"scope": "profile" to put it on each person's own screen instead,
which is also the only way you learn who pressed it.
Where they are drawn: the owner's half lives in the server's plugin card, in a browser, because installing and configuring a server is an owner-on-web job. The per-person half is on all seven surfaces natively, under Settings → Plugins in the apps, beside the accounts people have connected. You write one manifest and it renders everywhere, the same way your blocks do.
Ask for as little as possible
Every capability you declare becomes a line on the install screen, in plain language, before anyone decides. There are 3:
| Capability | The owner sees | What it permits |
|---|---|---|
http | Can contact example.com | Outbound HTTPS to the hosts you name. One leading `*.` wildcard label is allowed. Ports and schemes are not part of the grant: the host function only ever speaks https on the default port. Private, loopback and link-local addresses are refused AFTER DNS resolution, and every redirect hop is re-checked. An approved hostname is not a route into the owner's home network. |
storage | Can save its own settings and cached data | A key-value bucket isolated by plugin id. Removing the plugin removes the bucket. |
library | Can check whether specific titles are in your library, and never what else is there | The narrow lookup, and nothing else: you hand over external metadata ids (tmdb, imdb, tvdb, anilist, whatever your service uses) and get back local refs for the ones that exist here. There is deliberately no search, no listing and no filter behind this. You can ask whether titles you already know about are present; you cannot find out what else is. A full library-read grant was weighed for plugin pages and refused, because no page we wanted needed it. |
Implementing a family is not a capability. Implementing a family is declared through `provides`, not through a capability. Capabilities are ambient powers an owner weighs; implementing a host contract is not one, and a grant that grants nothing is noise on the install screen. A plugin that only transforms what it is handed asks for nothing at all, and the hub says so.
Review checks that what you ask for is proportionate to what you do, so a plugin that lists hosts it has no reason to contact comes back. It is also simply better for you: a plugin that asks for nothing installs without anyone hesitating.
Test it on a real server
Developer mode installs unsigned packages from a file, so you can run your own work
before anyone has signed it. Set MULTIPASS_PLUGIN_DEV=1 on the server,
restart it, and an Install from file control appears in
Settings → Plugins. The server announces developer mode at every
boot, because a server that will run unsigned code should never do so quietly.
multipass plugins pack ./myplugin -out myplugin.mpp # no key: unsigned
multipass plugins install myplugin.mpp # or use the hub
multipass plugins call com.example.myplugin match.movie -input '{"title":"Akira","year":1988}' plugins call is the fastest loop: it runs one op against your real
package and prints exactly what you returned.
Developer mode accepts an unsigned package. It never accepts a badly signed one: that is tampering or corruption, not a development flow.
Package it
A plugin ships as a .mpp: A zip holding manifest.json, manifest.sig, and every file the manifest names. Verification happens entirely in memory. A package that fails leaves nothing on disk.
multipass plugins pack ./myplugin -out myplugin.mpp Registry packages are signed for you, on approval, by the developer portal's publish key. You never need a key to submit, and you never hold one: the key that signs what the catalog serves is not yours and is not on your server.
Submit it
Listing is free and hosting is free. Submitting happens in the developer portal, signed in with your ordinary Multipass account: there is nothing separate to sign up for.
- Create the listing. Name, description, category, your public developer name, an icon and screenshots, and one line on why you need each capability you ask for. Those lines are shown to owners on the privacy card beside the grant itself, so write them for the person deciding.
- Upload the version. The built
.mpp, the source it was built from, and your build notes. Both are required: review reads the source against the package. - Test it on real servers, if you want to. An uploaded build is installable by you and by up to 10 testers you invite by email, through a per-account beta catalog. It is signed and consented exactly like a published plugin and appears in their hub marked Beta. Testing builds expire 90 days after upload.
- Submit when you are ready. Review is a person reading it, so expect a conversation rather than a verdict. A first submission gets a full pass; an update is read as a diff against your last approved version, so an update that asks for no new capability is a light review. A rejection comes with its reason and puts the version back in your hands.
You can withdraw a version while it is submitted or in review, and you can unlist a plugin later without deleting it.
What the registry will and will not list
- Grants must be proportional to purpose. A plugin that asks to contact hosts it has no reason to contact is sent back.
- Sources are reviewed for legitimacy. A plugin whose purpose is to make infringing material easy to obtain is refused, whatever it is called.
- It has to work: the package must install, load and answer its declared ops.
- Source is required at submission. A binary alone cannot be reviewed.
On approval the portal signs the package and publishes it, and it appears in every server's Settings → Plugins under Available. Nothing is pushed to anyone: an owner still chooses to install it.
Updates and withdrawal
Upload a new version in the portal the same way. A new version that wants a NEW power is flagged as a new decision rather than treated as consent to update. That is worth designing around: if you can avoid adding a capability in version 2, the update is one click for everyone who already has you installed.
A version can also name a minHostVersion, and an older server refuses
it rather than installing a plugin it cannot run. Set it whenever you use something
a recent server added, because the alternative is a plugin that installs and then
silently does nothing.
A signed kill switch can withdraw a plugin. Servers honor it on a daily check and whenever an owner opens their plugins page. A withdrawn plugin is switched off with its reason shown, cannot be re-enabled, and cannot be reinstalled. It is never deleted: the owner needs to see what happened, and a plugin's data outliving a bad release is worth more than a tidy directory.
We would only reach for that in the case it exists for: a plugin doing something to people's servers that it should not. It is not an editorial tool.
What plugins deliberately cannot do
Deliberately outside the plugin surface, recorded so nobody proposes them twice.
- Authentication providers
- Identity belongs to the accounts service. A plugin in the credential path would reopen exactly what sign-in enforcement closed.
- Transcoder hooks
- Plugin-supplied ffmpeg arguments are command injection with extra steps.
- Client-side plugin code
- A TV has no browser engine to run it in, and a per-client plugin UI contract is the thing that killed the last generation of media-server plugins. Plugins ship data; the apps render it.
Next
The full API reference How owners install and manage plugins