Plugin SDK
This is the developer reference for building a ROPARC plugin. For installing one, see Managing plugins; for using one on a dashboard, see Widgets & Plugins.
A plugin is a self-contained built app (HTML/JS/CSS) that runs inside a sandboxed iframe on a dashboard and talks to ROPARC over a postMessage bridge.
How plugins work
- A plugin is static files — there is no plugin server. Everything it needs from ROPARC goes through the bridge.
- The plugin never sees the auth token. Every
ctx.api.*call is a request the host runs on the viewer’s behalf, automatically scoped to the current view context (active baseline, change-request branch, or variant). A plugin shows the right data in every view mode for free. - Plugins are git-committed and version-pinned by the installer, so a dashboard viewed at a historic baseline serves the contemporaneous build.
initRoparcPlugin() and ctx
Two small, dependency-free files form the in-iframe SDK — copy them into
your plugin’s repo verbatim and build against RoparcPluginCtx:
pluginProtocol.ts— the postMessage contract + typesroparcPluginSdk.ts—initRoparcPlugin()returningctx
import { initRoparcPlugin } from './roparcPluginSdk';
const ctx = await initRoparcPlugin(); // resolves once the host sends `init`
initRoparcPlugin() announces readiness to the host and resolves with ctx
when the host replies. Outside a ROPARC host it never resolves — guard with
a timeout in development.
ctx fields
| Field | Type | Meaning |
|---|---|---|
ctx.workareaId | string | The active workarea. |
ctx.baseline | string | null | Active baseline name, or null at HEAD. |
ctx.dna | string | null | Active variant, or null. |
ctx.params | Record<string, unknown> | The widget’s Parameters (JSON) from the config dialog. |
ctx.dashboardId | string | null | The dashboard hosting this widget (null in previews). |
ctx.widgetId | string | This widget instance’s stable id — namespace your per-instance config by it. |
ctx.user | { id, displayName } | The viewer. |
ctx.theme | { mode: 'light' | 'dark' } | Current theme. |
ctx.api | RoparcPluginApi | Backend calls (below). |
ctx.ui | RoparcPluginUi | Host-rendered UI (rich text). |
ctx.onThemeChange(cb) | (mode) ⇒ unsubscribe | Subscribe to theme switches. |
ctx.setHeight(px) | (number) => void | Ask the host to size the iframe. |
ctx.api — backend calls
All return Promises and run with the viewer’s credentials at the current view context. Items and documents are queried with the same query language as the Items page.
| Method | Signature | Notes |
|---|---|---|
query | (jql?, {page?, pageSize?}) → {items, total, page, pageSize} | Query items. |
getItem | (id) → Item | |
createItem | (payload) → Item | {type, title, description?, custom?, links?, commitMessage?} |
updateItem | (id, payload) → Item | Partial; same shape as create. |
deleteItem | (id) → void | |
getLinks | (id) → {links_to, linked_from} | |
getDocument | (id) → Document | Includes sections. |
listDocuments | (jql?, {page?, pageSize?}) → {documents, total} | |
createDocument | (payload) → Document | |
updateDocument | (id, payload) → Document | |
getConfig | () → ResolvedWorkareaConfig | item types, link types, enums, fields. |
updateConfig | (patch) → config | Patches workarea config (for in-widget config wizards). |
getUsers | () → {users:[{id, display_name}]} | |
getCurrentUser | () → {id, display_name} | |
listAttachments | (entity, id) → {attachments:[…]} | entity = 'item' | 'document' | 'dashboard'. |
uploadAttachment | (entity, id, {filename, mimeType, dataBase64}) → {url, filename} | |
getAttachment | (entity, id, filename) → {mimeType, dataBase64} | Blob, base64-encoded. |
raw | (path, init?) → {status, ok, body} | Escape hatch — unversioned /api/... call, token injected by host, view context not auto-applied. |
ctx.ui — host-rendered rich text
A plugin can’t embed ROPARC’s editor (it lives in the host, and image uploads need the token), so the host renders it for you and returns plain data. This is how a plugin stores descriptions that render identically everywhere else in ROPARC.
| Method | Signature | Notes |
|---|---|---|
editRichText | (value, opts?) → string | null | Opens the host’s editor in a modal seeded with value (ROPARC rich-text JSON or ''). Resolves the edited JSON, or null on cancel. opts = {title?, placeholder?, attachmentTarget?}; pass attachmentTarget: {kind:'item'|'document', id} to enable image uploads. |
renderRichText | (value) → string | ROPARC rich-text JSON (or legacy markdown) → sanitised HTML for read-only display. |
Packaging a plugin
A plugin zip must contain an index.html at its root. A single wrapping
folder is auto-stripped, so zipping either dist/ or the contents of dist/
both work.
my-plugin.zip
├── index.html ← required, at the root
├── assets/
│ ├── app.js
│ └── app.css
├── manifest.json ← optional (see below)
└── icon.svg ← optional, referenced by manifest.icon
- Use relative asset paths (
./assets/app.js, not/assets/app.js). With Vite, setbase: './'. Assets are served under a per-session signed path, so absolute paths break. - Limits: 25 MB / 2000 files per bundle; icon ≤ 256 KB.
manifest.json (optional)
Describes how the plugin appears in the Add Widget picker. All fields optional:
{
"name": "Fault Tree",
"description": "Build and edit fault tree analyses traced to requirements.",
"icon": "icon.svg",
"defaultSize": { "w": 10, "h": 10, "minW": 5, "minH": 5 }
}
| Field | Default | Notes |
|---|---|---|
name | the plugin id | Tile title (shown instead of the raw id). |
description | "" | Tile subtitle. |
icon | none | Path relative to the plugin root; inlined as a data URI (≤ 256 KB). SVG or PNG. |
defaultSize | {w:8,h:8,minW:4,minH:4} | Initial grid footprint when dropped. |
Examples
Minimal plugin (vanilla, no bundler)
index.html:
<!doctype html>
<html>
<body style="font-family: sans-serif; margin: 12px">
<h3 id="title">…</h3>
<ul id="list"></ul>
<script type="module" src="./app.js"></script>
</body>
</html>
app.js:
import { initRoparcPlugin } from './roparcPluginSdk.js';
const ctx = await initRoparcPlugin();
document.getElementById('title').textContent =
`Hi ${ctx.user.displayName} — workarea ${ctx.workareaId}`;
const { items } = await ctx.api.query('type = "requirement"', { pageSize: 10 });
document.getElementById('list').innerHTML = items
.map((i) => `<li>${i.id} — ${i.title}</li>`)
.join('');
Read a configured parameter and react to theme
const ctx = await initRoparcPlugin();
// documentId comes from the widget's Parameters (JSON): { "documentId": "D-ROP-2" }
const docId = ctx.params.documentId;
const doc = docId ? await ctx.api.getDocument(docId) : null;
ctx.onThemeChange((mode) => (document.documentElement.dataset.theme = mode));
Edit a description through ROPARC’s own editor
const item = await ctx.api.getItem('ROP-42');
const edited = await ctx.ui.editRichText(item.description ?? '', {
title: 'Edit ROP-42',
attachmentTarget: { kind: 'item', id: 'ROP-42' }, // images upload onto this item
});
if (edited !== null) {
await ctx.api.updateItem('ROP-42', { description: edited, commitMessage: 'Edit via plugin' });
}
// Display stored rich text read-only:
panel.innerHTML = await ctx.ui.renderRichText(item.description ?? '');
Per-instance configuration
A plugin dropped on two dashboards is two instances that may need different
settings. Rather than cramming everything into ctx.params, store the
instance’s config as a dashboard attachment keyed by ctx.widgetId:
const KEY = `${ctx.widgetId}.config.json`;
async function loadConfig() {
if (!ctx.dashboardId) return {}; // preview / unsaved dashboard
try {
const a = await ctx.api.getAttachment('dashboard', ctx.dashboardId, KEY);
return JSON.parse(atob(a.dataBase64));
} catch {
return {}; // not set yet
}
}
async function saveConfig(cfg) {
if (!ctx.dashboardId) return;
await ctx.api.uploadAttachment('dashboard', ctx.dashboardId, {
filename: KEY,
mimeType: 'application/json',
dataBase64: btoa(JSON.stringify(cfg)),
});
}
The plugin renders its own settings UI and persists the result as a
git-backed attachment. Namespacing by widgetId keeps two instances on the same
dashboard from clobbering each other.
Local development
Iterating through upload → install on every change is slow. In development
builds only, the Add Widget dialog offers Plugin (by URL · dev) and the
config dialog offers a Custom URL… option, so you can point a widget at a
local dev server (e.g. a Vite server at http://localhost:5180/) and get hot
reload. This path is stripped from production builds — a deployed dashboard
can only ever load an installed, git-served plugin, never an arbitrary URL. When
ready, build your plugin and install the zip from
Settings → Plugins.
Security
- Plugins run as the viewer. A plugin can do nothing the viewer couldn’t do through the API with their own session — there is no elevation.
- The token never crosses the bridge.
ctx.api.*calls are run by the host. - Installing is privileged — workarea installs need
widgets:author; group and organization installs need a site administrator. See Managing plugins.