The category in a plugin is not a label. It is the routing rule.
Sixty two first party packages ship today, in exactly six categories: 27 widget, 15 library, 10 ui, 5 graphics, 3 dbs and 2 framework. The category is not a naming convention and it is not a tag. It is a plugin_type field on the plugin record, and three separate systems read it: the editor decides which socket the plugin turns up in, validation decides which files it has to export, and the runtime decides which of three execution surfaces its code runs on. Nothing is wired up by hand after an install. One package in the catalog proves the field is the authority rather than the name, because its directory and its exports both say widget while its published npm name says ui, and every surface in Studio routes it by the field. A new project starts with two plugins already installed, the native data driver and the Tailwind framework. Everything past those two is one install, and the category decides where it lands.

Plugin packages shipped, across exactly six categories
Methods declared by the 42 packages that export any, over 632 fields
Filenames the platform probes for after an install. Most ship two
Packages that ship server code. No widget or ui plugin does
Six categories, and the category decides both ends.
Install one and it turns up in the right place with nothing to configure, because a single field is read by six different pieces of the editor and by three different runtime entry points. The picture below is the contract, not the picker: on the left is how a plugin of that category is reached, and on the right is the exact export the runtime calls and with what signature. Read it as six independent contracts hanging off one field rather than as one plugin interface with six flavours, because that is what it is. A widget and a ui plugin do not export the same shape, do not receive the same arguments and do not appear in the same place, and a graphics plugin does not even ship the file the other five build around.
One contract, six shapes, and the empty cells are the argument.
After the npm install the platform probes for six filenames, index.mjs, setup.mjs, studio.mjs, runtime.mjs, server.mjs and style.css. It looks at the package root first and then inside dist, records which of the two it found so the runtime knows whether to also load the built stylesheet, and then imports every file it found except style.css and server.mjs. That import is the real does this load check: a plugin whose index.mjs throws while being imported fails validation right there and lands with the error text stored on its record, which is why a broken plugin shows you a reason rather than a blank slot. The table below is what the six categories actually ship. The crosses carry as much of the argument as the text does: the server.mjs column is empty for every browser facing category, which is the fact the execution surfaces and the third party gate both rest on. One file is left out of the table deliberately. studio.mjs is shipped by exactly one package in the whole catalog, the native data driver, where it is a separate design time server surface for the editor, so it gets this sentence instead of a column of five crosses.
index.mjs must export | runtime.mjs must export | server.mjs | setup.mjs | |
|---|---|---|---|---|
| widget, 27 shipped | methods, plus optional capabilities | one function per method | ||
| library, 15 shipped | methods | one stub per method | 12 of 15 | 12 of 15 |
| ui, 10 shipped | properties | fn, and optionally destroy | ||
| graphics, 5 shipped | tabs | |||
| dbs, 3 shipped | tabs and public_filter | 3 of 3 | ||
| framework, 2 shipped | tabs, core and the UI primitives | core, mandatory. resources, optional |
An install is a real npm install into a directory that belongs to your project.
Nothing here is a metaphor for an install. An entry is written into the map of purchased plugins on the project, carrying the installed flag, the version, the name of the parent plugin where there is one, the file manifest, the plugin type and the background flag. Then npm install runs inside a plugins directory belonging to this project alone, with every argument shell quoted. Then that directory is rsynced out to the regional servers. Then the installed package is scanned to depth ten and every file is read into a CouchDB database that belongs to the project, keyed by its relative path, and an existing setup is carried across the update rather than overwritten.
- Every project gets its own node_modules tree, and a package that fails to fetch removes only itself. There is a note in the code saying why: an earlier version deleted the whole tree on a failure, which uninstalled every other plugin the project had because one package could not be downloaded.
- Uninstalling flips the installed flag, runs npm uninstall, and blanks the project side plugin document down to its id, its revision and its setup. Configuration survives an uninstall and a later reinstall, which is the difference between removing a plugin and losing the credentials you typed into it.
- Nothing is bundled into an app build. The runtime fetches each plugin file per session over a /plugins route, which serves out of the per project tree and requires either a resolvable app id or a valid token. For an unbundled source file that route rewrites the relative ESM imports on the way out, so a browser can load them at all. What deploy bundles is the setup map, collected into the app startup data.
- In a dev session the runtime tries src before dist for any .mjs file and falls back to dist, so editing plugin source shows up without a rebuild. Everywhere else the manifest decides, and where it records that the built copy was the one found, dist wins.
- The record is writteninstalled, version, parent name, file manifest, plugin type and the background flag
- npm install runsInto the plugins directory for this project alone, every argument shell quoted
- The tree is synced outThat project plugin directory is pushed to the remote regional servers
- The package is snapshottedEvery file read into the project database, keyed by its relative path
A child plugin names its parent. The parent has to name the child back.
A plugin may declare one parent, and the platform refuses to install it unless two separate declarations agree. The child names the parent through a dependency field on its own record. The parent has to list the child back, in an authorized plugins array on the parent record. Neither declaration is enough on its own. The checks run in order and each failure comes back as a numbered refusal rather than a stack trace, and all of them run before npm is invoked, so a rejected install leaves nothing behind on disk. The same two checks are re-run later, on every read and every write of a plugin configuration, and again on the server before a plugin method is allowed to execute.
The plugin decides what its own configuration may tell the browser.
Plugin configuration is not held in the shared registry. It lives as a setup document inside the CouchDB database of the project that configured it, written by the setup form and read back by both sides. When the read is a public one, the platform imports the plugin and calls a function the plugin exports itself, and sends back only what that function returned. There is no platform wide allow list of safe keys, and that is both the strength of the arrangement and its limit: the plugin author is the one who knows which of the fields is the secret, and the plugin author is the one who writes the filter.
Three surfaces, and the server one is checked before the import, not after.
Plugin code runs in exactly three places. In the browser, as a widget that owns a container the runtime made for it, as a ui plugin decorating an element you already had, as a framework, or as a background plugin hooked onto the app lifecycle. On a dedicated plugin service, its own process on port 3008, for library server methods. And on a separate dispatch for the data drivers, which never goes through that service and caches its live connection per app and per plugin. Both server paths execute under a sixty second ceiling.
- A library method is two files doing two jobs. The browser stub casts the declared fields, enforces the mandatory ones and posts; the server file does the work and holds the credential. That split is the whole reason a Stripe secret key has no browser path: it is read in server.mjs, out of the setup document handed in, and nowhere else.
- Before the service loads a plugin server file it asks whether the publisher is first party, and it asks before the import rather than after. Importing a module already runs its top level code, so a check placed after it checks nothing. Third party server methods are refused by default and the switch that would allow them is set to false in both configurations, so third party plugins are a browser side product surface today.
- A background plugin gets five lifecycle hooks, fired in the order beforeInit, initialized, beforeMounted, mounted and systemReady around app start, each one handed the session, the app object, the api and the setup data. The run in background checkbox is offered only on a library plugin, and registration is skipped entirely in slim bundle mode.
- The fields a method declares do double duty. On a Full Stack VPS the platform walks the installed plugins and registers every method as an MCP tool, named plugin then method, with its input schema built from those same declared fields and its description taken from the tooltip the method already carried.
- The workflow step firesNaming a plugin, a method and the properties for it
- The browser stub validatesFields cast to their declared types, the mandatory ones enforced
- It posts to the plugin serviceThe token checked against a live session in your own project database, never from a cache
- The server file runs, with a ceilingSixty seconds, with the setup document handed in
A browser plugin is handed one object, and this is all of it.
The runtime builds one parameters object per call and hands it in as the second argument. A widget does not reach for a global to find the platform, and there is no separate plugin API to learn beyond what arrives in that object. The host creates the container div, reads the plugin id and the method name off the node, imports index.mjs and checks the declared capabilities against the current environment, resolves and casts every declared field and enforces the mandatory ones, loads style.css and dist/runtime.css when the manifest recorded them, fetches the plugin setup, and only then imports runtime.mjs and awaits the method. A throw inside that method is caught and reported rather than left to take the page down. The shape below is the widget one. A ui plugin gets something different enough that it is worth printing beside it, because that difference is the whole distinction between the two categories: a widget is given a container it owns, a ui plugin is given an element you already had.
The developer path is npm, and nothing about it is homegrown.
There is no proprietary toolchain here and no plugin CLI to learn. The whole path is a published sandbox package you develop against, a plain Vite library build, and npm. The platform only meets your plugin at the end of it, when you register the package name.
- 01
Develop against the sandbox
The harness is a real published package, @xuda.io/plugin-sandbox at version 1.0.27. It builds a two pane page: a Plugin Configurator form on the left, generated from the fields your method declares, a drag resizer in the middle, and a Live Preview pane on the right with the container div your method will be handed. A dropdown picks which of your methods to test. Any field declaring the editor render type gets a real Monaco editor wired to its change event, with the language taken from the render params and javascript as the default. Adoption is partial and worth stating plainly: thirteen of the sixty two shipped packages develop against it, and twenty ship a test page of their own under tests.
- 02
Build with Vite, into dist
Every sandbox using package carries the same vite config. The library entry is src/runtime.mjs, the format is ES, dynamic imports are inlined, the output lands in dist, any emitted stylesheet is renamed to runtime.css, and a small copy step moves everything in src except runtime.mjs into dist alongside it. That convention is exactly why the platform records a dist flag and a css flag per file when it probes your package, and why the runtime knows to load dist/runtime.css as well as the module.
- 03
Publish, then register it
All sixty two shipped package.json files carry the identical publish script: npm version patch, then npm publish. Plain semver with nothing layered on top, and the high water marks say how much use that gets. The native data driver is on 1.0.320, the Tailwind framework on 1.0.128, the AI app builder on 1.0.74. Registering with the platform is a separate step that pulls the live manifest from npm, takes the version from there rather than from your form, and takes the marketplace icon out of the svg_icon field in your package.json if you left the icon on the form blank.
Or describe it, and the plugin system builds a plugin with a plugin.
The builder is not a tool the chat assistant can reach on its own. Both of its methods are marked private, so nothing exposes them as agent tools; the plugins tab stages the action and the assistant runs it as part of that job. The thing that does the work is itself a shipped library plugin, the studio AI app builder, called the same way any other library method is called. Every constraint below is in the generator rather than in a policy document, and the terminal state is a private plugin, on purpose.
Five actions, and no two of them are gated the same way.
Two of these are routinely described as one thing, and they are not. An Agency membership gates registering a plugin at all, and it sits on the upload method. An active Stripe Connect seller account gates going public, and it sits on the publish method, which anyone signed in may call. They stop different people at different moments. Installing a plugin onto a project is gated by neither: it needs admin clearance on that project, and if the plugin carries a price it routes through a payment step first.
Clearance | Membership plan | The other gate | |
|---|---|---|---|
| Install or uninstall on a project | Project admin | A priced plugin routes through a payment step first | |
| Read or write a plugin configuration | Project admin | The parent chain is rechecked before anything is touched | |
| Register a plugin at all | Project admin | Agency | Ownership enforced on every update |
| Publish it to the marketplace | Any signed in user | An active Stripe Connect seller account, or a 403 | |
| Archive it | Project admin | Owner only |
Publishing is a separate action from updating the code, deliberately.
Uploading a new version, or letting the AI builder rebuild one, never re-enters review. The existing lifecycle is carried forward verbatim: a plugin that was private stays private, a plugin that was public stays public, and the review state it already held is the review state it keeps. Going public is a different method with a different clearance, run once, by the owner, on purpose, and it is the only call that runs the gate. Everything below hangs off that one call.
Nine things the plugin system does not do.
- Third party plugins cannot run server code today. The interim policy is deny, it is a config switch, and that switch is off in both configurations, so anything you build for someone else to install is a browser side surface only.
- The widget contract carries a headless capability, for plugin code that runs where there is no document. Not one of the 62 shipped packages declares it. The contract is there, the shipped proof is not.
- The runtime supports an optional destroy export on a ui plugin. None of the ten shipped ui plugins exports one.
- Two of the three data drivers are read only as shipped. Only the native one implements all six services. MySQL and CouchDB export a read and a connection test and nothing else, and asking either for anything more throws by naming the missing service.
- The icon picker searches inside the kit you have selected, not across the kits you have installed. And a graphics plugin may declare at most one npm dependency, which is why every shipped kit is one upstream package and its directory tree.
- Installing a shared component checks only two kinds of plugin dependency, the interface framework on a program and the data driver on a table. A component that uses a widget plugin does not declare it, so that install can succeed with the widget still missing.
- The AI builder is guarded before the code is packed, never while it runs. Thirteen forbidden patterns and a fixed enum of filenames, both applied to text. There is no runtime isolation waiting behind them.
- Nothing in the platform decides which configuration keys are safe to publish. That judgement is delegated wholly to the plugin author, and a plugin that makes no judgement hands its whole configuration document to the browser.
- The registration path accepts two more type strings than the product exposes, and nothing in the catalog ships either of them. The six in the picker are the six that route anywhere.
The questions you are forming right now
Five of these six answers end on a limit. They are here because you would meet them in the first week anyway.
Watch a plugin install and appear in its socket.
Install two plugins and you have a data layer and a design system.
That is literally what a new project ships with: the native data driver and the Tailwind framework, and nothing else. Everything past those two is one install, and the category on the record decides where it turns up, whether that is the insert panel, the icon picker, the table driver select, a switch on one element, or the workflow action list.