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(...)andcodeboltPlugin.onMessage(...) - plugin uses
plugin.dynamicPanel.onMessage(...)andplugin.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.updatedevents
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.pathat 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.