Xuda Plugins

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.

Six categories, six different sockets
A child plugin needs its parent to name it back
Third party server code is refused by default
The category in a plugin is not a label. It is the routing rule.
62

Plugin packages shipped, across exactly six categories

111

Methods declared by the 42 packages that export any, over 632 fields

6

Filenames the platform probes for after an install. Most ship two

15

Packages that ship server code. No widget or ui plugin does

The routing rule

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.

Two footnotes, both checkable. The category is a field on the record and not the word in the package name, and one package proves the difference: its directory and its contract say widget, its published npm name says ui, and every socket above routes it by the field. And the Installed badge above the insert panel does not count all six categories. It counts framework, graphics and widget only, because those three are the ones that contribute something droppable.
The contract

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 shippedmethods, plus optional capabilitiesone function per method
library, 15 shippedmethodsone stub per method12 of 1512 of 15
ui, 10 shippedpropertiesfn, and optionally destroy
graphics, 5 shippedtabs
dbs, 3 shippedtabs and public_filter3 of 3
framework, 2 shippedtabs, core and the UI primitivescore, mandatory. resources, optional
Installing

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.
One install
  1. The record is written
    installed, version, parent name, file manifest, plugin type and the background flag
  2. npm install runs
    Into the plugins directory for this project alone, every argument shell quoted
  3. The tree is synced out
    That project plugin directory is pushed to the remote regional servers
  4. The package is snapshotted
    Every file read into the project database, keyed by its relative path
Composition

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 pair decides two things beyond permission. A child has its configuration lookup redirected to the parent document, so there is exactly one Stripe configuration rather than one per widget that draws a card field. And it decides trust: because the server merges the parent server file into the same object as the child, a first party plugin that depends on a third party one counts as third party.
Configuration

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.

The honest reading is that this boundary is exactly as good as the plugin that drew it. An absent filter means an unfiltered document, and the platform does not second guess that. The same setup document is what the deploy step collects, one entry per installed plugin, into the startup data an app is built with.
Execution

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.
One library call
  1. The workflow step fires
    Naming a plugin, a method and the properties for it
  2. The browser stub validates
    Fields cast to their declared types, the mandatory ones enforced
  3. It posts to the plugin service
    The token checked against a live session in your own project database, never from a cache
  4. The server file runs, with a ceiling
    Sixty seconds, with the setup document handed in
What plugin code can reach

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.

runtime.mjs · export async function <method>(fields, params)
fields the declared fields, cast to their types and checked
params:
SESSION_ID, method, _session
dsP, sourceP, propsP datasource, source row, resolved props
plugin_name
container_node the div the runtime made for this widget
container_data
plugin_setup the filtered setup, never the raw document
report_error(desc, warn)
log_error(desc, warn)
call_plugin_api(name, data) the only route to the server
set_SYS_GLOBAL_OBJ_WIDGET_INFO(doc)
run_widgetCallbackEvent() fires the event id you declared as a field
api_utils
a ui plugin gets a different shape entirely:
fn(plugin_name, element, properties) no container, it decorates yours
This is a calling convention, not a sandbox. Widget code runs in your page like any other script on it, and what the object buys you is that the platform decides what goes in. A ui plugin may also export destroy, to release whatever it built when its element goes away, and the runtime replays the exact properties fn was handed rather than re-resolving them at teardown, because by then the datasource behind that row may already be retired.
Building one

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.

  1. 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.

  2. 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.

  3. 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.

The other path

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.

The versions state the intent: a created plugin is 0.1.0 and a modify bumps the patch. It gets its own npm scope rather than the first party one, it installs from the artifact rather than from the registry, and going public is a separate explicit action on the plugin card, which is what the assistant reply says in as many words. Now the honest correction. That scan is a static pattern match over generated text, sitting on top of a response schema and a seven filename allow list. It is not dynamic analysis and it is not a sandbox.
Who may do what

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 projectProject adminA priced plugin routes through a payment step first
Read or write a plugin configurationProject adminThe parent chain is rechecked before anything is touched
Register a plugin at allProject adminAgencyOwnership enforced on every update
Publish it to the marketplaceAny signed in userAn active Stripe Connect seller account, or a 403
Archive itProject adminOwner only
The gate

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.

Ten checks come back from the reviewer, each carrying its own severity: bot activity, undisclosed commercial intent, harmful code, suspicious activity, a malformed account name, spam and low quality, impersonation and trademark, prohibited content, plagiarism and duplication, and leaked secrets or personal data. What the reviewer is handed is worth stating plainly, because it sets the ceiling on what any verdict can mean: the name, the description, the tags, the category, the price, and a code summary that is the npm package name, its version and the platform list. It is a metadata review, not a read of your source. A first party publisher skips it outright. Anything held goes to a person, with an operator email, a support ticket deduped on the item id, and an appeal link on a rejection, and there is no turnaround target written anywhere in the code.
Honest limits

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.
Before you commit

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

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.