Remote Plugins
Hosted tools that talk to the live editor
Overview
Remote plugins are hosted web apps that open inside the confBuild editor. They run in an iframe, exchange messages with the editor, and can read project or sheet data, write controlled sheet updates, run registered script actions, and show their own UI.
Hosted Anywhere
Deploy the plugin on your own HTTPS domain and add it in Project Settings > Plugins.
Sheet Access
Use bridge methods such as sheet.getActive, sheet.getRange, sheet.setCell, and sheet.applyOperation.
Toolbar and Script Launch
Place plugins in the main menu, tools toolbar, or Tools dropdown, and open them from API.openPlugin().
Plugin Manifest
Project Settings > Plugins stores plugin definitions in project.plugins. Each plugin declares its iframe URL, trusted origin, menu placement, and permissions.
project.plugins = [
{
id: "bom-helper",
name: "BOM Helper",
iframeUrl: "https://plugins.example.com/bom/index.html",
allowedOrigin: "https://plugins.example.com",
icon: "fa-solid fa-table",
placement: "project-settings",
permissions: ["project:read", "sheet:read", "sheet:write", "script:run-action"],
order: 20,
enabled: true
}
];topbarIn the scene header, left of the Project Type buttontools-toolbarBeside Tools entries such as BOM, CAM, and Floor Planproject-settingsInside the upper-right Tools dropdown near Project SettingsManage Plugins
Open Project Settings > Plugins to add, duplicate, remove, disable, order, import, export, and validate remote plugins without editing the project JSON directly.
- Use Catalog to install verified confBuild plugin listings into the current project.
- Leave Origin empty to derive it from the iframe URL, or enter the exact URL origin manually.
- Choose where the launch button appears: Main Menu, Tools Toolbar, or Project Settings menu.
- Grant only the permissions required by the remote app.
- Use the JSON tray to export the current manifest or import a single plugin, an array, an object map,
{ "plugins": [...] }, or{ "installed": [...] }. - Saving is blocked when a plugin has an invalid ID, duplicate ID, unsafe URL, mismatched origin, unsupported placement, invalid icon, or invalid dialog size.
Plugin Catalog
The Catalog button in Project Settings > Plugins shows published Plugin Store listings that can be installed into the project with one click. A catalog install writes the same manifest shape as a manually added plugin, so catalog plugins keep the same permission checks, approval dialogs, toolbar placements, and script access.
Verified Listings
Listings are curated from the admin Plugin Store and include the trusted iframe origin, requested permissions, placement, size, and hosted URL.
Project Scoped
Installing a catalog plugin adds it to the current project's project.plugins array. Other projects stay unchanged.
Same Runtime API
Catalog plugins use the same iframe bridge, sheet API, approval gate, audit trail, and API.openPlugin() scripting hooks.
Admin Plugin Store
Admins can manage catalog listings at /admin/plugin-store. Published listings appear in the Project Settings catalog; unpublished listings stay hidden from users.
- Create or edit listings with publisher, version, category, tags, and summary.
- Set the iframe URL, exact allowed origin, placement, icon, dialog size, and permissions.
- Use Seed Built-ins to add bundled example listings to the store.
- Only admins can create, edit, unpublish, or delete listings.
Example Plugins
The docs site includes static example plugins you can register as remote plugins or install from the Plugin Catalog. The hosted example list is available at /assets/plugins/.
{
"id": "sheet-inspector-example",
"name": "Sheet Inspector Example",
"iframeUrl": "https://confbuild.com/assets/plugins/sheet-inspector-example/",
"allowedOrigin": "https://confbuild.com",
"icon": "fa-solid fa-table",
"placement": "project-settings",
"permissions": ["project:read", "sheet:read", "sheet:write", "script:run-action"],
"order": 30,
"enabled": true,
"width": "1180px",
"height": "820px"
}The example reads project and sheet data, writes a single cell, runs a script action, shows a toast, closes itself, and handles host calls from API.callPlugin().
{
"id": "cut-list-planner-example",
"name": "Cut List Planner Example",
"iframeUrl": "https://confbuild.com/assets/plugins/cut-list-planner-example/",
"allowedOrigin": "https://confbuild.com",
"icon": "fa-solid fa-ruler-combined",
"placement": "tools-toolbar",
"permissions": ["project:read", "sheet:read", "sheet:write"],
"order": 35,
"enabled": true,
"width": "1240px",
"height": "860px"
}The Cut List Planner example reads sheet rows, groups material usage, estimates stock sheets and cost, writes a summary cell, and handles host calls for refresh, focus, pricing, and export.
API.openPlugin("sheet-inspector-example");
await API.callPlugin("sheet-inspector-example", "setMessage", {
message: "Called from a project script"
});
await API.callPlugin("sheet-inspector-example", "highlightRows", {
rowIndexes: [0, 3],
outputIds: ["plate_1"]
});
API.openPlugin("cut-list-planner-example");
await API.callPlugin("cut-list-planner-example", "focusMaterial", {
material: "birch plywood"
});
const cutPlan = await API.callPlugin("cut-list-planner-example", "exportPlan");AI Coding Prompt
Copy this prompt into your coding assistant to adapt an existing app into an external plugin, including the hosted iframe connection, manifest files, CAD row writes, and a manual test checklist.
You are an expert frontend engineer adapting an existing frontend app into a confBuild remote plugin.
Goal:
Adapt the existing app in this repository into a hosted iframe plugin for confBuild: [DESCRIBE_THE_PLUGIN].
Existing app: [DESCRIBE_THE_EXISTING_APP, for example "an I-beam sizing/configurator app"].
Preserve the app's current purpose, user workflow, UI structure, styling, framework, build system, components, and business logic unless a specific change is required for confBuild integration.
Hard adaptation rules:
- First inspect the existing files and identify the current entry point, framework, state model, UI controls, and build/deploy commands.
- Do not create a new app, new project folder, fresh scaffold, unrelated demo, or replacement UI unless this repository is empty or I explicitly ask for a fresh app.
- Do not replace the existing app with a generic confBuild sample. Add the confBuild bridge, manifest/discovery metadata, and sheet-writing workflow to the existing app.
- Keep existing domain-specific behavior. For example, if this is an I-beam app, keep the I-beam controls/calculations and map its outputs to confBuild ibeam rows.
- Make the smallest coherent edits needed. Reuse existing buttons/forms where possible; add plugin actions beside the existing workflow.
- Use the existing app's tech stack and package setup. Use plain HTML, CSS, and JavaScript only when no framework/build setup already exists.
- Keep the app runnable standalone outside confBuild with realistic demo data.
confBuild plugin facts:
- The plugin runs inside a sandboxed iframe and communicates with the confBuild editor through a postMessage bridge.
- Prefer loading https://confbuild.com/js/confbuild-plugin-client.js and creating the client with ConfBuildPluginClient.create({ pluginId: "[PLUGIN_ID]" }).
- Development may use http://localhost:[PORT]/. Production iframe URLs must use HTTPS.
- The manifest allowedOrigin must exactly match the iframeUrl origin.
- Manifest fields: id, name, iframeUrl, allowedOrigin, icon, placement, permissions, order, enabled, width, height, description.
- Project Settings > Plugins can import a plugin from a pasted URL if that URL returns manifest JSON or an HTML page with discovery metadata.
- To support URL import, publish a production plugin-manifest.json next to the plugin and add <link rel="confbuild-plugin-manifest" href="./plugin-manifest.json"> to index.html.
- If a separate manifest file is not available, include <script type="application/confbuild-plugin+json">...</script> or meta tags for confbuild:plugin-id, confbuild:plugin-name, confbuild:plugin-url, confbuild:plugin-origin, confbuild:plugin-icon, confbuild:plugin-description, confbuild:plugin-placement, confbuild:plugin-permissions, confbuild:plugin-order, confbuild:plugin-width, and confbuild:plugin-height.
- Always expose a plugin description for URL import. confBuild reads it from plugin-manifest.json description, embedded plugin JSON description, or HTML metadata. Include at least <meta name="confbuild:plugin-description" content="[PLUGIN_DESCRIPTION]">; also include <meta name="description" content="[PLUGIN_DESCRIPTION]"> for normal SEO/browser previews. Accepted description metadata names include confbuild:plugin-description, confbuild-plugin-description, description, og:description, and twitter:description.
- Plugin hosts must allow cross-origin reads for the page or manifest so browser-based URL import can read the metadata.
- Placements: topbar, tools-toolbar, project-settings.
- Permissions: project:read, sheet:read, sheet:write, script:run-action, app-action:run. Request only the permissions needed.
- Helper methods: ready, getCapabilities, getProjectSummary, getActiveSheet, getSheetData, getRange, getCell, setCell, applyOperation, runScriptAction, runAppAction, toast, close, onHostCall.
- Sheet writes, applyOperation, script actions, and app actions require edit mode and trigger a confBuild approval dialog.
- If you implement postMessage manually instead of using the helper, validate event.source and event.origin.
Before finishing, verify confBuild plugin installability:
- Publish a real /plugin-manifest.json next to the app and link it with <link rel="confbuild-plugin-manifest" href="./plugin-manifest.json">.
- Ensure the manifest iframeUrl is the final canonical deployed URL, not a preview/stale Cloud Run URL.
- Ensure allowedOrigin and confbuild:plugin-origin are exact iframe origins. Use localhost origins for local dev, such as http://localhost:5173.
- Add HTML fallback metadata for id, name, description, placement, permissions, width, and height.
- Serve HTML and manifest with CORS readable from confBuild, at minimum Access-Control-Allow-Origin: https://app.confbuild.com or * for public static metadata.
- Load https://confbuild.com/js/confbuild-plugin-client.js.
- Wait for ready robustly: const session = await (typeof client.ready === "function" ? client.ready() : client.ready);.
- In standalone mode, keep export simulated/demo-only. In confBuild mode, label the action Write to confBuild.
- For writes, show a preview, then call sheet.applyOperation using append_after_marker and marker OUTPUTID.
- Test in an iframe host: plugin emits confbuild:plugin:ready, receives confbuild:plugin:init, and sends confbuild:plugin:request.
- Test localhost import using http://localhost:[PORT]/ without requiring HTTPS.
confBuild sheet/CAD row facts:
- Geometry is spreadsheet-driven. Output rows live below an OUTPUTID marker.
- A geometry block starts with a header row whose first cell is "#", second cell is "type", and remaining cells are parameter names.
- Data rows under that header use first cell as output id, second cell as row type, then values matching the header.
- Do not mix row types under one header. When switching type, write a new "#" header.
- Position and rotation columns should be last as x, y, z, rx, ry, rz. Units are millimeters and degrees unless noted.
- Material strings can be "#RRGGBB" or "physicalMaterialId;#RRGGBB;roughness;metalness;reflectivity;opacity".
- Prefer SnippetManager-compatible headers because cadnodeengine maps rows by header names.
- For new straight profile members (ibeam, uprofile, tprofile, lprofile, squaretube, roundtube, aluprofile), prefer global startPoint/endPoint columns when endpoints, nodes, contact faces, or spans are known. The renderer derives length and aligns local +Z from start to end; use length/x/y/z/rx/ry/rz mainly for legacy headers or special local rotation.
- Use sheet.applyOperation with append_after_marker, insert_before_marker, replace_range, replace_sheet, or batch. For generated geometry, append_after_marker after OUTPUTID is usually safest.
Common geometry row schemas from cadnodeengine/SnippetManager:
- preferred straight profile endpoint schemas:
ibeam/uprofile/tprofile/lprofile/squaretube/roundtube: ["#", "type", "profile", "material", "startPoint", "endPoint"]
aluprofile: ["#", "type", "series", "size", "material", "startPoint", "endPoint"]
- cube: ["#", "type", "width", "length", "height", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- cylinder: ["#", "type", "radiusTop", "radiusBottom", "height", "radialSegments", "heightSegments", "openEnded", "hollow", "wallWidth", "thetaStart", "thetaLength", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- cone: ["#", "type", "radius", "height", "radialSegments", "heightSegments", "openEnded", "thetaStart", "thetaLength", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- sphere: ["#", "type", "radius", "widthSegments", "heightSegments", "phiStart", "phiLength", "thetaStart", "thetaLength", "material", "x", "y", "z", "rx", "ry", "rz"]
- plate: ["#", "type", "width", "length", "height", "baseplane", "extrusionvector", "gridsize", "material", "edges", "x", "y", "z", "rx", "ry", "rz"]
- wall: ["#", "type", "width", "depth", "height", "gridsize", "material", "edges", "x", "y", "z", "rx", "ry", "rz"]
- extrusion: ["#", "type", "baseplane", "extrusionvector", "material", "bevelwidth", "bevelsegments", "edges", "x", "y", "z", "rx", "ry", "rz"]
- shape: ["#", "type", "points", "splinePath", "splineDivisions", "depth", "material", "edges", "x", "y", "z", "rx", "ry", "rz"]
- nurbs: ["#", "type", "controlPoints", "rows", "cols", "degreeU", "degreeV", "segments", "depth", "material", "x", "y", "z", "rx", "ry", "rz", "showControlPoints", "showControlGrid"]
- sheetmetal: ["#", "type", "contour", "thickness", "innerRadius", "kFactor", "bends", "stampings", "sheetMetalKernel", "renderMode", "showFlatPattern", "material", "x", "y", "z", "rx", "ry", "rz"]
- tube: ["#", "type", "path", "radius", "bendradiusfactor", "material", "x", "y", "z", "rx", "ry", "rz"]
- line: ["#", "type", "p1", "p2", "width", "material", "x", "y", "z", "rx", "ry", "rz"]
- ring: ["#", "type", "outerRadius", "innerRadius", "depth", "material", "edges", "x", "y", "z", "rx", "ry", "rz"]
- spring: ["#", "type", "radius", "pitch", "turns", "wireRadius", "length", "material", "x", "y", "z", "rx", "ry", "rz"]
- ibeam/uprofile/tprofile: ["#", "type", "profile", "width", "height", "length", "flangeThickness", "webThickness", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- lprofile: ["#", "type", "profile", "width", "height", "length", "thickness", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- squaretube: ["#", "type", "profile", "outerWidth", "outerHeight", "length", "wallThickness", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- roundtube: ["#", "type", "profile", "outerDiameter", "length", "wallThickness", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"]
- aluprofile: ["#", "type", "series", "size", "length", "material", "edges", "price_per_meter", "total_price", "supplier", "currency", "weight_per_meter", "estimated_price", "cross_section_area", "x", "y", "z", "rx", "ry", "rz"]
- dinpart: ["#", "type", "partType", "diameter", "length", "material", "hostOutputId", "seatFace", "axis", "entryDirection", "throughHole", "x", "y", "z", "rx", "ry", "rz"]
- subtraction: ["#", "type", "cuttedoutputids", "cutoutputids", "x", "y", "z", "rx", "ry", "rz"]
- union: ["#", "type", "mergedoutputids", "x", "y", "z", "rx", "ry", "rz"]
- connector: ["#", "type", "label", "type", "activator", "inparent", "context", "refpos", "refrot", "x", "y", "z", "rx", "ry", "rz", "sx", "sy", "sz"]
- dimensionline: ["#", "type", "p1", "p2", "width", "material", "orthogonalLength", "orthogonalOffset", "dashed", "rotation", "textSize", "textBackground", "editcell", "targetOutputId", "targetSeries", "refpos", "x", "y", "z", "rx", "ry", "rz"]
- 3dlabel: ["#", "type", "text", "position", "target", "background", "color", "borderColor", "borderWidth", "lineColor", "lineWidth", "fontSize", "padding", "align"]
- arrow: ["#", "type", "start", "end", "width", "material", "headLength", "headWidth", "x", "y", "z", "rx", "ry", "rz"]
- ledstrip: ["#", "type", "path", "ledspacing", "ledsize", "ledcolor", "glowintensity", "glowradius", "pcbwidth", "pcbheight", "pcbcolor", "x", "y", "z", "rx", "ry", "rz"]
Geometry selection rules:
- Use cube only for true rectangular prisms or simple mitered ends via startSlope/endSlope.
- Use extrusion or plate with baseplane for triangular gussets, trapezoids, brackets, non-rectangular panels, holes, slots, and constant-thickness custom outlines.
- Use cylinder/cone/sphere/ring/tube/spring for native round or path-based forms.
- Use ibeam, uprofile, tprofile, lprofile, squaretube, roundtube, or aluprofile for standard structural/profile members instead of approximating them with cubes.
- For plugin-generated structural/profile members, use endpoint rows first when you know start/end nodes. Example: ["beam_1", "ibeam", "IPE 200", "#708090", "(0,0,2800)", "(6000,0,2800)"].
- Use dinpart for standard purchased hardware such as screws, nuts, washers, bearings, springs, fittings, rails, and furniture connectors.
- For holes and cutouts, create visible or hidden cutter rows first, then add a subtraction row. Keep subtraction x/y/z/rx/ry/rz at 0 and position the cutter rows.
- Use nurbs only for smooth freeform skins. Do not use NURBS for ordinary chamfers or prismatic brackets.
- For sheet metal, use sheetmetal with contour, bends JSON, and stampings JSON rather than many loose plates.
If the plugin includes a prompt box for users:
- Convert the user's natural-language request into previewable confBuild rows using the schemas above.
- Show a row preview and a human summary before calling applyOperation.
- Never send sheet/project data to an external AI service unless the user configured that service and explicitly triggers generation.
- Do not put API keys in browser code. If external AI is required, call a user-owned backend.
- Validate every generated row before writing: unique output ids, known type, matching header length, sensible dimensions, and no mixed types under one header.
Example geometry write:
await confbuild.applyOperation({
operation: {
mode: "append_after_marker",
marker: "OUTPUTID",
data: [
["#", "type", "width", "length", "height", "material", "edges", "startSlope", "endSlope", "x", "y", "z", "rx", "ry", "rz"],
["plugin_base", "cube", 420, 240, 30, "steel_s235;#6f7c85;0.35;0.85;0.8;1", true, 0, 0, 0, 0, 15, 0, 0, 0],
["#", "type", "baseplane", "extrusionvector", "material", "bevelwidth", "bevelsegments", "edges", "x", "y", "z", "rx", "ry", "rz"],
["plugin_gusset", "extrusion", "(0,0);(160,0);(0,120)", "(0,0,8)", "steel_s235;#4f6f7a;0.35;0.8;0.8;1", 1, 1, true, -120, 0, 30, 0, 0, 0]
]
}
});
Requirements:
1. Adapt the existing app UI for [FEATURES_AND_WORKFLOW] instead of replacing it with a new plugin UI.
2. Include loading, connected, standalone demo, empty, and error states without breaking the existing app flow.
3. On startup, wait for confbuild.ready, read getCapabilities(), and enable only actions allowed by granted permissions.
4. If not running inside confBuild within 2 seconds, use realistic demo data so the UI can be developed standalone.
5. Read sheet data with getActiveSheet({ includeFormulas: true }), getRange(), getCell(), or getSheetData() as appropriate.
6. For writes, use the smallest safe operation: setCell for one cell, applyOperation for structured multi-cell updates.
7. Before any write/action button runs, show the user exactly what will change.
8. Register host-call handlers with confbuild.onHostCall() for [HOST_CALLS_OR_NONE].
9. Use responsive, accessible UI. Avoid external dependencies unless necessary.
10. Do not store secrets, tokens, project data, or sheet data in localStorage.
11. Catch bridge errors and show clear messages. Use toast() for successful writes when available.
12. Make the production URL self-describing for Project Settings URL import with either the manifest link, embedded JSON manifest, or confBuild meta tags above.
Plugin config:
- id: [PLUGIN_ID]
- name: [PLUGIN_NAME]
- description: [PLUGIN_DESCRIPTION]
- dev iframeUrl: http://localhost:[PORT]/
- production iframeUrl: [HTTPS_PLUGIN_URL]
- placement: [PLACEMENT]
- permissions: [PERMISSIONS]
- icon: [FONT_AWESOME_ICON_CLASS]
- dialog size: [WIDTH] x [HEIGHT]
Deliverables:
- index.html
- Optional styles.css and app.js if the code is easier to maintain split into files
- plugin-manifest.dev.json
- plugin-manifest.production.json
- index.html discovery metadata: either a confBuild manifest link, embedded application/confbuild-plugin+json, or confBuild plugin meta tags
- README with hosting and Project Settings > Plugins installation steps
- Manual test checklist covering open, read, write, approval cancel, host call response, error state, and close dialog
Before coding, make reasonable defaults for any missing placeholder, list those defaults, then write the files.Client Helper
Remote plugins can load the small browser helper from https://confbuild.com/js/confbuild-plugin-client.js or copy it into their own bundle. It handles init, capabilities, requests, timeouts, and host-to-plugin calls.
<script src="https://confbuild.com/js/confbuild-plugin-client.js"></script>
<script>
const confbuild = ConfBuildPluginClient.create({ pluginId: "bom-helper" });
async function main() {
const session = await confbuild.ready;
const capabilities = await confbuild.getCapabilities();
const activeSheet = await confbuild.getActiveSheet({ includeFormulas: true });
console.log("Available bridge methods", capabilities.methods);
await confbuild.setCell({
address: "B2",
value: "Updated from plugin"
});
}
confbuild.onHostCall("highlightRows", async payload => {
return { highlighted: payload.outputIds?.length || 0 };
});
main().catch(error => console.error(error));
</script>Message Bridge
The helper wraps this protocol. If you implement the bridge yourself, the editor sends an init message to the iframe, and the plugin sends typed requests back to the parent window. The init message includes capabilities for the current session, and project summary data only when the plugin has project:read.
let confbuildOrigin = "";
let session = null;
const pending = new Map();
window.addEventListener("message", event => {
const message = event.data;
if (message?.type === "confbuild:plugin:init") {
confbuildOrigin = event.origin;
session = message;
return;
}
if (message?.type === "confbuild:plugin:response") {
const callbacks = pending.get(message.requestId);
if (!callbacks) return;
pending.delete(message.requestId);
message.ok ? callbacks.resolve(message.result) : callbacks.reject(new Error(message.error));
}
});
parent.postMessage({
type: "confbuild:plugin:ready",
pluginId: "bom-helper"
}, "*");
function request(method, payload = {}) {
if (!session) throw new Error("Plugin session is not ready yet.");
if (!confbuildOrigin) throw new Error("confBuild origin is unknown.");
const requestId = crypto.randomUUID();
parent.postMessage({
type: "confbuild:plugin:request",
version: 1,
sessionId: session.sessionId,
pluginId: session.pluginId,
requestId,
method,
payload
}, confbuildOrigin);
return new Promise((resolve, reject) => pending.set(requestId, { resolve, reject }));
}Read and Write Sheets
Read operations require sheet:read. Write operations require sheet:write and edit mode.
const activeSheet = await request("sheet.getActive", { includeFormulas: true });
const priceRange = await request("sheet.getRange", { range: "A1:D20" });
await request("sheet.setCell", {
address: "B2",
value: "Updated from plugin"
});
await request("sheet.applyOperation", {
operation: {
mode: "replace_range",
range: "A10:C11",
data: [
["plugin_part", "cube", 120],
["plugin_plate", "cube", 40]
]
}
});Open from Scripts
Plugins can be launched from the VBA-like scripting API. Host-to-plugin calls work when the iframe is already open and the remote app implements a response handler.
API.registerAction({
id: "openBomHelper",
label: "Open BOM Helper",
run: async () => API.openPlugin("bom-helper", { source: "script-action" })
});
await API.callPlugin("bom-helper", "highlightRows", {
outputIds: ["plate_1", "plate_2"]
});Security Rules
- Use HTTPS for hosted plugins. Localhost HTTP is accepted only for development.
allowedOriginmust exactly match the iframe URL origin.- The editor checks both the iframe window and origin before accepting a message.
- Grant only the permissions the plugin needs.
- Sheet writes, structured sheet operations, script actions, and app actions show a structured approval dialog once per method while the plugin dialog is open.
- The plugin dialog keeps a compact per-session audit trail of inbound bridge methods and outbound host calls without storing request payloads.
- Avoid untrusted same-origin plugin pages because iframe sandbox isolation is weaker for same-origin content.