Skip to main content

Plugin-Backed UIs

Sometimes the UI should not talk directly to the server. Instead, the UI talks to a plugin, and the plugin acts as the application backend for that interface.

This is the pattern shown by sample-plugins/feedback-form-plugin, and a lighter packaging form of it appears in sample-plugins/simpleui-plugin.

The Bridge Model

In a plugin-backed UI, the browser side uses the injected codeboltPlugin bridge. There are two useful ways to structure that communication:

Pattern 1: Fetch And Router

Use this when the UI action is naturally a request with a response.

Browser side:

const response = await codeboltPlugin.fetch('/feedback', {
method: 'POST',
json: formData,
});

const payload = await response.json();

Plugin side:

const router = plugin.dynamicPanel.router(PANEL_ID);

router.post('/feedback', async (c) => {
const body = await c.req.json();

return c.json({
success: true,
message: `Thanks, ${body.name}`,
});
});

This gives you a clean application contract for:

  • forms
  • settings saves and loads
  • command buttons
  • CRUD-like panel actions

Pattern 2: Async Message Passing

Use this when the interaction is event-driven, streaming, or conversational.

Browser side:

codeboltPlugin.sendMessage({
type: 'start-sync',
projectId: 'abc123',
});

codeboltPlugin.onMessage((message) => {
console.log(message);
});

Plugin side:

plugin.dynamicPanel.onMessage(PANEL_ID, async (message) => {
if (message.type === 'start-sync') {
await plugin.dynamicPanel.send(PANEL_ID, {
type: 'sync-status',
status: 'running',
});
}
});

This works well for:

  • streaming progress
  • dashboards with continuous updates
  • long-running workflows
  • multi-turn back-and-forth
  • custom protocols that do not map cleanly to a single request and response

What Both Patterns Share

Both patterns keep the same separation of concerns:

  • the UI owns presentation and user interaction
  • the plugin owns backend logic and state transitions

Choosing Between Them

Treat the panel bridge like an internal transport and choose the interaction style that matches the shape of the workflow:

  • UI uses codeboltPlugin.fetch(path, init)
  • plugin uses plugin.dynamicPanel.router(panelId)
  • UI uses codeboltPlugin.sendMessage(...) and codeboltPlugin.onMessage(...)
  • plugin uses plugin.dynamicPanel.onMessage(...) and plugin.dynamicPanel.send(...)

Use fetch/router when:

  • the UI action naturally maps to a request and a response
  • you want route-level structure in the plugin
  • you want handlers that feel like application backend endpoints

Use async messages when:

  • the plugin needs to push updates independent of a request
  • the workflow is long-running or streaming
  • the interaction is conversational or stateful in a custom way

You can also mix them in one application:

  • fetch/router for form submissions and commands
  • raw async messaging or events for status, progress, and live updates

Events With The Fetch Pattern

If you use fetch/router for request/response but still need push updates, you can combine it with event-style notifications:

  • UI: codeboltPlugin.on('event.name', handler)
  • plugin: c.emit('event.name', payload) from a router handler

Why This Pattern Exists

Both patterns are useful when the plugin needs to:

  • validate or transform UI input
  • manage application state for the panel
  • mediate calls into the rest of Codebolt
  • push live updates back into the UI
  • encapsulate plugin-owned workflows behind a narrow UI contract

Feedback Form Pattern

The feedback form sample now demonstrates the fetch/router version:

  • the UI is plain HTML and JavaScript
  • the UI submits through codeboltPlugin.fetch('/feedback', ...)
  • the plugin handles the request through plugin.dynamicPanel.router(panelId).post('/feedback', ...)
  • the plugin pushes live stats back through stats.updated events

This is a good starter template for plugin-backed forms, control panels, or operator consoles.

Wrapping A Larger UI With A Plugin

simpleui-plugin shows a related packaging pattern:

  • a larger compiled SPA is built separately
  • the plugin manifest points ui.path at that built UI
  • the plugin can be almost empty if it only exists to host the application surface

That is useful when:

  • you want a big React/Vue UI inside Codebolt
  • you want to distribute that UI as a plugin
  • you may later add richer plugin-side backend logic without changing the overall deployment shape

Manifest Shape

The essential manifest field is:

{
"codebolt": {
"plugin": {
"triggers": [{ "type": "manual" }],
"ui": {
"path": "./ui/default/index.html"
}
}
}
}

That tells Codebolt the plugin owns a UI surface that should be loaded through the plugin runtime.

When To Prefer The Plugin Backend Pattern

Prefer this pattern over direct server access when:

  • the UI is tightly coupled to one plugin capability
  • the plugin needs to act as the domain backend
  • you want a small, explicit message contract instead of broad server API access
  • the UI lives inside Codebolt and should be mediated by plugin lifecycle

Prefer fetch + router over raw messages when:

  • the UI action naturally maps to a request and a response
  • you want route-level separation in the plugin
  • you want handlers that feel like application backend endpoints
  • you want easier migration from web-app thinking into plugin-backed panels

Prefer raw sendMessage/onMessage only when the interaction is inherently event-driven or conversational.

If your panel needs both, use both. The bridge supports mixing request/response and async messaging in the same UI.

See Also