capabilities
{ browser: boolean, headless: boolean }. The runtime's supports_current_environment check reads this. Widgets default to browser only; libraries may enable both.
Xuda Developer Plugins
This page is the canonical reference for the plugin shape. Each section maps a concept to the exact
runtime function that reads it. When the runtime disagrees with the docs, the runtime wins; point a PR
at the relevant file under runtime/js/.
Plugin package names follow @xuda.io/xuda-<category>-plugin-<name>. The scope
proves marketplace origin; the category slug decides dispatch; the name slug is yours to own.
dbs # database adapter framework # CSS framework / shell UI graphics # icon / asset pack library # callable server or client library ui # behavioral decorator on an existing element widget # full visual widget that owns its container
@xuda.io/xuda-dbs-plugin-couchdb @xuda.io/xuda-framework-plugin-tailwind @xuda.io/xuda-graphics-plugin-feather-icons @xuda.io/xuda-library-plugin-firebase-auth @xuda.io/xuda-ui-plugin-moment @xuda.io/xuda-widget-plugin-chartjs
The loader (xuda-project-loader-module.esm.js) builds a manifest object per
plugin. Each manifest entry records whether a file exists, whether it was bundled (in dist/),
and whether it is considered empty.
plugin.manifest = {
"runtime.mjs": {
exist: true,
dist: true, // bundled via vite, imported from /dist/runtime.mjs
is_empty: false,
css: true, // set if dist/runtime.css exists
},
"index.mjs": {
exist: true,
dist: true,
is_empty: false,
},
"style.css": {
exist: false, // consulted for widget plugins in the browser
},
};
xuda-project-loader-module.esm.jsconst infer_draft_plugin_manifest_entry = function (plugin_name, plugin_doc, dist_path, source_path, extra = {}) {
const has_file = (file_path) => !!plugin_doc?.files?.[`${plugin_name}/${file_path}`];
if (has_file(dist_path)) return { exist: true, dist: true, is_empty: false, ...extra };
if (has_file(source_path)) return { exist: true, dist: false, is_empty: false, ...extra };
return null;
};
Everything else in index.mjs is ignored. Keep the file pure.
capabilities{ browser: boolean, headless: boolean }. The runtime's supports_current_environment check reads this. Widgets default to browser only; libraries may enable both.
methodsA map of method id to { name, tooltip, fields, response, event_name_for_successful_callback?, field_id_to_return_result? }. Used by widget and library.
propertiesA flat map of property id to field definitions. Used by ui plugins, which do not have methods — the decorator always runs a single function against the element.
tabsUsed two ways. Graphics plugins export a tabs array of SVG groups. Dbs and framework plugins export a tabs(doc) function returning a setup schema keyed by tab id.
public_filterOptional on dbs plugins. Given the current setup doc, returns a redacted version safe to send to the client (passwords stripped, connection strings masked).
core / bind / toast / …Reserved for framework plugins. Classes that implement the global UI primitives. The active framework plugin's exports become the default for the whole app.
The same fields schema is used by the studio form generator, the sandbox harness, and the
runtime's get_fields_data coercion layer. Stick to these shapes and the whole pipeline
will understand your plugin.
{
label: "Chart Type", // required, human label
type: "string" | "number" | "boolean" | "array" | "object",
tooltip: "…", // shown next to the label
mandatory: true, // studio validation
defaultValue: () => "line_chart",// literal or function
render: "select" // input renderer hint
| "segment"
| "checkbox"
| "code"
| "field"
| "eventId"
| "editor",
render_params: { lang: "js" }, // per-renderer config
options: [ // for select/segment
{ label: "Line Chart", value: "line_chart" },
],
helper: "<a href='...'>Color Generator</a>", // raw HTML hint
size: 12, // grid span in studio tabs
placeholder: "…",
}
The sandbox and the runtime coerce the raw form value using the field's type. Booleans become true/false, numbers use parseFloat, arrays and objects are JSON.parsed.
Both render a choice between options. select is a dropdown, segment is a horizontal button group, typically used with booleans.
Renders a Monaco editor. The defaultValue is the starter code, render_params.language is the syntax mode. Used by framework plugins for theme configs.
Studio-aware pickers. eventId lets authors pick an event to invoke on success. field lets authors pick a datasource field to write a return value into.
The runtime resolves plugin modules through func.utils.get_plugin_resource. It picks
among the studio draft, the local plugins/ folder, and the CDN URL based on the
environment.
runtime/js/xuda_utils.jsfunc.utils.get_plugin_resource = function (SESSION_ID, plugin_name, plugin_resource) {
// Studio draft with local files
const local_plugin_resource_url =
await func.utils.get_local_studio_plugin_resource_url(SESSION_ID, plugin_name, plugin_resource);
if (local_plugin_resource_url) {
return await import(/* @vite-ignore */ local_plugin_resource_url);
}
// Published app: relative path into the plugins directory
if (_session.app_mode === 'dev') {
return `../../plugins/${plugin_name}/${resource}?ts=${resource_ts}`;
}
// Production: CDN path
return `https://${_session.domain}/plugins/${plugin_name}/${resource}?app_id=${_session.app_id}&ts=${resource_ts}`;
};
Some plugins need config that is unique per installed app: a Firebase project ID, a Twilio API key,
a Stripe secret, a theme token. That config lives in the app's setup doc, not in the plugin source.
The runtime fetches it via func.utils.get_plugin_setup and passes it into the method.
func.utils.get_plugin_setup = function (SESSION_ID, plugin_name) {
// 1. Check the local cache
const cached = await db.get(`cache_plugin_setup_${plugin_name}`);
if (cached) return normalize_setup_response(cached);
// 2. Fetch from the runtime server
const json = normalize_setup_response(await func.common.db(SESSION_ID, 'get_plugin_setup', {
plugin_name,
}));
// 3. Cache it for next time
await db.put({
_id: `cache_plugin_setup_${plugin_name}`,
data: json,
docType: 'cache_plugin',
});
return json;
};
params that reaches your method. A Firebase Auth method, for
example, receives the project's Firebase config as params.plugin_setup without having to
know where it came from.
If your plugin ships a dist/runtime.css, the loader records manifest["runtime.mjs"].css = true
and func.runtime.widgets.load_runtime_css appends the stylesheet into <head>
the first time the plugin is rendered. No explicit import required in your JS.
xuda_runtime.browser.widgets.jsif (_plugin.manifest['style.css'].exist) {
func.runtime.widgets.load_css_style(widget_context);
}
// later:
await func.runtime.widgets.load_runtime_css(widget_context);
server.mjs entry point.
Database plugins and library plugins that call privileged services (billing, email, AI) ship a
server.mjs. The runtime server imports it, passes it session context, and uses the
named exports to fulfill the operation.
server.mjsimport { createRequire } from 'module';
import _ from 'lodash';
import crypto from 'node:crypto';
const require = createRequire(import.meta.url);
export async function read(e, docP, table_obj) { /* ... */ }
export async function upsert(e, docP, table_obj) { /* ... */ }
export async function delete_(e, docP, table_obj) { /* ... */ }
export async function check_unique(e, docP, table_obj) { /* ... */ }
xuda-library-plugin-studio-ai-app-builder) also uses
server.mjs to run OpenAI calls with the app owner's credentials and mutate the studio
database directly. Same contract, different intent.
tests/ is a vite harness only. It is never loaded at runtime and should not be included in the npm package beyond what .npmignore allows.
The loader does not scan package.json#main or exports. It strictly loads dist/runtime.mjs, dist/index.mjs, dist/runtime.css, and (if present) server.mjs.
There is no preinstall / postinstall contract. Plugins are static packages; the loader rejects anything that needs build steps on the consumer.
Exports not listed on this page are ignored. Keep index.mjs to the five names; everything else is dead code.