RFC: Frontend Dynamic Plugin System
Summary
Proposes a frontend dynamic plugin system that allows Python backend extensions to also contribute UI components (panels, settings pages, sidebar widgets) that are discovered and loaded at runtime — without requiring frontend code changes or rebuilds.
Motivation
DeerFlow's backend has a mature extension system allowing operators to drop in Python packages that contribute middleware, task lifecycle hooks, services, and FastAPI routers. However, the frontend has no equivalent mechanism — all UI extension points are static and require code changes + rebuilds.
This creates an asymmetry: backend behavior is pluggable, but frontend UI is not. Operators who want to customize the UI (dashboards, debugging tools, domain-specific panels) must fork the frontend.
Goals
- Allow backend extensions to also declare UI components in their Python package
- Enable runtime discovery of available UI components via the Gateway API
- Keep frontend asset build pipeline separate from the Python extension author workflow
- Maintain security boundaries (no arbitrary code execution, authenticated access)
- Follow existing extension patterns so the mental model is consistent with the backend
Non-Goals
- This RFC does not cover skill/package management UI (already exists via
/api/skills) - This RFC does not cover building a full app store or marketplace
- This RFC does not require the frontend to be bundled with extension code
Proposed Architecture
Overview
Extension Package Gateway (Backend) Frontend (Next.js)
deerflow-extension-example/ → 1. Loads Python extension → 1. Calls GET /api/extensions
├── __init__.py via install() to discover plugins
├── ui_manifest.json → 2. Serves ui_manifest.json → 2. Fetches manifest
└── static/dist/bundle.js via /api/extensions/{name}/ for each extension
3. Serves static assets → 3. Dynamically imports
via /api/extensions/{name}/ JS bundle from Gateway
static/ 4. Registers components
with plugin registry
5. Renders in appropriate
slotsExtension Manifest Format
Each extension with UI components includes a ui_manifest.json at the package root:
{
"$schema": "https://docs.deerflow.ai/schemas/ui-manifest.json",
"name": "example",
"version": "0.1.0",
"components": [
{
"id": "example-panel",
"type": "panel",
"label": "Example Panel",
"icon": "chart-bar",
"entry": "static/dist/bundle.js",
"config_schema": {
"type": "object",
"properties": {
"apiKey": { "type": "string", "secret": true }
}
}
}
]
}Supported Component Types
| Type | Description | Render Location |
|---|---|---|
panel |
A custom panel in the right panel area | Chat page right panel (alongside artifacts, browser view) |
settings_page |
A settings configuration page | Settings dialog |
sidebar_widget |
A widget in the workspace sidebar | Workspace sidebar |
API Endpoints
GET /api/extensions
Returns all loaded extensions that have UI components.
{
"extensions": [
{
"name": "example",
"version": "0.1.0",
"ui_components": [
{
"id": "example-panel",
"type": "panel",
"label": "Example Panel",
"icon": "chart-bar"
}
],
"config_schema": { "type": "object", "properties": {} }
}
]
}GET /api/extensions/{name}/ui_manifest.json
Returns the raw UI manifest for a specific extension.
GET /api/extensions/{name}/static/{path}
Serves static assets (JS bundles, images) from the extension package. Path traversal is blocked.
Bundle Build Requirements
Extension authors must:
- Write UI as a React component bundled with Vite/esbuild (ES2020, self-contained)
- Include the bundle in their Python package under
static/dist/ - Reference the bundle path in
ui_manifest.json
Example Vite config:
export default defineConfig({
build: {
lib: {
entry: 'src/panel.tsx',
formats: ['es'],
fileName: 'bundle',
},
rollupOptions: {
external: ['react'],
},
},
});Security Considerations
- No arbitrary code execution — Gateway serves files from controlled paths, no path traversal
- Authentication — all
/api/extensions/*endpoints require session auth (401 otherwise) - Config secrets — fields marked
secret: trueare redacted before sending to frontend - CSP — dynamic script loading must respect CSP headers (needs further investigation)
Open Questions for Community Feedback
Should extension UI bundles run in iframes or the main thread?
- Iframe isolation is safer but harder to style/communicate with
- Main thread is simpler but a buggy bundle could break the whole app
How should extensions handle state persistence?
- Gateway API (
/api/extensions/{name}/state), localStorage, or read-only?
- Gateway API (
Should extensions be able to contribute route pages (new URLs)?
- Opens security and complexity questions
Should the manifest support version negotiation?
- Or keep it simple: if manifest exists, assume compatibility
How should bundle build errors be handled in development?
- Hot reload of bundles is an open problem
What component types should be in scope for MVP?
- Panel is the natural first target; settings page and sidebar widget are secondary
Implementation Phases
Phase 1 (MVP): Gateway endpoint GET /api/extensions, static file serving, frontend plugin registry, dynamic panel loading
Phase 2: Config schema validation, secret redaction, frontend config UI, state persistence API
Phase 3: Settings page + sidebar widget component types
Phase 4: Extension scaffolding tool, development mode with hot reload
Related Documentation
Full RFC document: docs/plans/2026-09-17-frontend-dynamic-plugin-rfc.md
Looking forward to feedback from the community!
Source: bytedance/deer-flow