Spaces:
Build error
Backend Plugin System Architecture
Technical architecture documentation for backend plugins in the PDF-TEI Editor.
Note: This guide covers backend plugins (Python code running on the server). For frontend plugins (JavaScript code running in the browser), see plugin-system-frontend.md. For practical backend plugin development, see Backend Plugin Development Guide.
Key Differences:
- Backend plugins: Python modules in
fastapi_app/plugins/that provide server-side functionality and API endpoints - Frontend plugins: JavaScript classes in
app/src/plugins/that extend the UI and handle client-side logic
Architecture
Backend plugins are Python modules discovered at runtime from:
fastapi_app/plugins/<plugin_id>/- Paths in
FASTAPI_PLUGIN_PATHSenvironment variable (colon-separated)
Each plugin:
- Inherits from
fastapi_app.lib.plugin_base.Plugin - Defines metadata (id, name, description, category, version, required_roles)
- Implements endpoints as async methods
- Has access to
PluginContextfor app state and user info
Creating a Plugin
Directory Structure
Use underscores in directory names (not hyphens) to avoid Python import issues:
fastapi_app/plugins/my_plugin/
├── __init__.py
├── plugin.py # Main plugin class
├── routes.py # Optional custom routes
├── my-script.js # Optional: frontend JavaScript
├── html/ # Optional: static files (auto-mounted)
│ ├── styles.css
│ └── template.xslt
└── tests/ # Plugin tests
├── test_plugin.py # Python unit tests
└── script.test.js # JavaScript unit tests (if applicable)
Static File Serving
Plugins can serve static files (CSS, JS, XSLT, images) by placing them in an html/ subdirectory. The PluginManager automatically mounts this directory during plugin discovery.
Pattern:
- Put files in:
fastapi_app/plugins/{plugin_id}/html/ - Files are served at:
/api/plugins/{plugin_id}/static/
Example:
fastapi_app/plugins/grobid/html/bibl-struct.xslt
→ Served at: /api/plugins/grobid/static/bibl-struct.xslt
Use cases:
- XSLT stylesheets for XML transformations
- CSS/JS assets for plugin HTML pages
- Static templates or data files
Frontend access:
// In frontend extension
const xsltString = await sandbox.fetchText('/api/plugins/grobid/static/bibl-struct.xslt');
Implementation: See PluginManager._try_mount_plugin_static_files() in plugin_manager.py.
Test Discovery: The smart test runner automatically discovers tests in plugin tests/ directories. Use @testCovers annotations to link tests to plugin files for dependency-based test execution.
Example Test Annotation:
"""
Unit tests for My Plugin.
@testCovers fastapi_app/plugins/my_plugin/plugin.py
"""
Minimal Plugin
# plugin.py
from fastapi_app.lib.plugins.plugin_base import Plugin
from typing import Any
class MyPlugin(Plugin):
@property
def metadata(self) -> dict[str, Any]:
return {
"id": "my-plugin",
"name": "My Plugin",
"description": "What it does",
"category": "analyzer", # Used for UI grouping
"version": "1.0.0",
"required_roles": ["user"] # or ["*"] for all
}
def get_endpoints(self) -> dict[str, callable]:
return {
"execute": self.execute,
"info": self.get_info
}
async def execute(self, context, params: dict) -> dict:
"""Main execution endpoint"""
# Access user: context.user
# Access app: context.app
return {"result": "data"}
async def get_info(self, context, params: dict) -> dict:
"""Info endpoint"""
return {"plugin": self.metadata["id"]}
Plugin Registration
In __init__.py:
from .plugin import MyPlugin
plugin = MyPlugin()
Conditional Availability
Plugins can define runtime availability conditions using the is_available() class method. This allows plugins to be conditionally loaded based on:
- Environment variables (e.g., application mode)
- External dependencies
- Configuration settings
- System capabilities
import os
from fastapi_app.lib.plugins.plugin_base import Plugin
class MyPlugin(Plugin):
# ... metadata and endpoints ...
@classmethod
def is_available(cls) -> bool:
"""Only available in development and testing modes."""
app_mode = os.environ.get("FASTAPI_APPLICATION_MODE", "development")
return app_mode in ("development", "testing")
When to Use:
- Development/testing-only plugins (like sample_analyzer)
- Plugins requiring optional external services
- Feature-flagged functionality
- Environment-specific tools
Behavior:
- Unavailable plugins are skipped during discovery (not registered)
- Default implementation returns
True(always available) - Checked once at startup during plugin discovery
Example - Mock Extractor Pattern:
Similar to mock_extractor.py:
@classmethod
def is_available(cls) -> bool:
"""Available only in testing mode."""
app_mode = os.environ.get("FASTAPI_APPLICATION_MODE", "development")
return app_mode == "testing"
Plugin Configuration with Environment Variables
Plugins often need configuration that can be set via environment variables or config keys.
Initialization Pattern
Initialize configuration values at plugin registration time in __init__.py. This ensures config keys are created from environment variables when the plugin is loaded:
# __init__.py
from fastapi_app.lib.plugins.plugin_tools import get_plugin_config
# Initialize config values from environment variables
get_plugin_config("plugin.my-plugin.enabled", "MY_PLUGIN_ENABLED", default=False, value_type="boolean")
get_plugin_config("plugin.my-plugin.api-key", "MY_PLUGIN_API_KEY", default=None)
get_plugin_config("plugin.my-plugin.timeout", "MY_PLUGIN_TIMEOUT", default=30, value_type="number")
from .plugin import MyPlugin
plugin = MyPlugin()
Access configuration in plugin methods using get_config():
# plugin.py
from fastapi_app.lib.utils.config_utils import get_config
class MyPlugin(Plugin):
async def execute(self, context, params: dict) -> dict:
config = get_config()
api_key = config.get("plugin.my-plugin.api-key")
timeout = config.get("plugin.my-plugin.timeout", default=30)
# Use config values...
Access configuration in custom routes using get_config():
# routes.py
from fastapi_app.lib.utils.config_utils import get_config
@router.get("/action")
async def custom_action():
config = get_config()
api_key = config.get("plugin.my-plugin.api-key")
# Use config values...
Priority: Config file (data/db/config.json) > Environment variable > Default value
Key points:
- Initialize config in
__init__.pyusingget_plugin_config()(creates keys from env vars) - Access config everywhere else using
get_config()(retrieves existing keys) - Config values are automatically created from environment variables on first initialization
- Routes and plugin methods use the same
get_config()pattern
Example - Plugin availability based on config:
@classmethod
def is_available(cls) -> bool:
"""Only available if enabled in config."""
from fastapi_app.lib.plugins.plugin_tools import get_plugin_config
enabled = get_plugin_config(
"plugin.my-plugin.enabled",
"MY_PLUGIN_ENABLED",
default=False,
value_type="boolean"
)
if not enabled:
return False
# Check if required configuration is present
api_key = get_plugin_config(
"plugin.my-plugin.api-key",
"MY_PLUGIN_API_KEY",
default=None
)
if not api_key:
return False
return True
Reference Implementation: See local_sync plugin for complete example.
Service Registry
Plugins can register and consume services by capability name, enabling loose coupling between plugins. Instead of depending on a specific plugin, consumers request services by what they do (e.g., "structured-data-extraction").
# Register a service during initialization
from fastapi_app.lib.services.service_registry import get_service_registry
service_registry = get_service_registry()
service_registry.register_service(MyExtractionService())
# Consume a service by capability
service = context.get_service("structured-data-extraction", ExtractionService)
if service:
result = await service.extract(model="model-id", prompt="Extract data", ...)
See Service Registry for complete documentation.
Role-Based Access
required_roles: ["admin"]- Only admin usersrequired_roles: ["user"]- Any authenticated userrequired_roles: ["*"]- Everyone (including anonymous)required_roles: []- Everyone (including anonymous)
Wildcard * in user roles grants access to all plugins.
Frontend Integration
Plugins appear in toolbar dropdown, organized by category. Frontend calls:
// List plugins (role-filtered)
const plugins = await api.getBackendPlugins();
// Execute plugin
const result = await api.executeBackendPlugin(
'my-plugin',
'execute',
{ param: 'value' }
);
Multi-Endpoint Menu Support
Plugins can define multiple menu entries, each calling a different endpoint with different parameters from the application state:
@property
def metadata(self) -> dict[str, Any]:
return {
"id": "my-analyzer",
"name": "Document Analyzer",
"description": "Analyzes documents",
"version": "1.0.0",
"category": "analyzer",
"required_roles": ["user"],
"endpoints": [
{
"name": "analyze",
"label": "Analyze Current XML",
"description": "Analyze currently open XML document",
"state_params": ["xml", "variant"]
},
{
"name": "analyze_all",
"label": "Analyze All Documents",
"description": "Run analysis on all documents",
"state_params": []
},
{
"name": "info",
"label": "Plugin Info",
"description": "Get plugin information",
"state_params": []
}
]
}
Endpoint Definition Fields:
name(required): Endpoint method name (must match key inget_endpoints())label(required): Display label for menu itemdescription(optional): Tooltip textstate_params(required): List of state fields to pass as parameters (see app/src/state.js)
Available State Parameters:
pdf- PDF document IDxml- XML document IDdiff- Diff XML document IDxpath- Current XPath selectionvariant- Variant filtercollection- Current collection ID- Other fields from
ApplicationStatetypedef
Backward Compatibility:
- If
endpointsnot defined: Single menu item callsexecuteendpoint - If
endpointsis empty array: Plugin appears in list but adds no menu items
Example with State Parameters:
async def analyze(self, context, params: dict) -> dict:
"""Analyze XML document from state parameters"""
xml_id = params.get("xml") # Passed from frontend state
variant = params.get("variant") # Passed from frontend state
if xml_id:
# Load and analyze the XML file
from fastapi_app.lib.core.dependencies import get_db, get_file_storage
from fastapi_app.lib.repository.file_repository import FileRepository
db = get_db()
file_repo = FileRepository(db)
file_storage = get_file_storage()
file_metadata = file_repo.get_file_by_id_or_stable_id(xml_id)
if file_metadata and file_metadata.file_type == "tei":
content_bytes = file_storage.read_file(file_metadata.id, "tei")
text = content_bytes.decode("utf-8")
# Perform analysis on text
return {"analysis": "result"}
return {"error": "No XML document open"}
API Endpoints
GET /api/v1/plugins- List plugins (filtered by user roles)POST /api/v1/plugins/{plugin_id}/execute- Execute endpoint
Request body:
{
"endpoint": "execute",
"params": {"key": "value"}
}
Custom Routes (Optional)
Add routes.py for custom FastAPI routes. Plugin routes use the unversioned /api/plugins prefix:
from fastapi import APIRouter
# Router prefix uses unversioned /api/plugins
router = APIRouter(prefix="/api/plugins/my-plugin", tags=["my-plugin"])
@router.get("/custom")
async def custom_route():
return {"custom": "data"}
This creates the endpoint at /api/plugins/my-plugin/custom.
Path Requirements:
- Router prefix should be
/api/plugins/{plugin-id}(unversioned) - The router is registered directly on the app (not under the versioned api_v1 router)
- Plugin routes are unversioned and independent of the main API versioning
- Plugin routes are excluded from the generated API client (
api-client-v1.js)
Frontend Access:
Plugin routes should be called using the callPluginApi method from BackendPluginsPlugin, not the main callApi function:
// In a plugin or component that has access to BackendPluginsPlugin
const backendPluginsPlugin = /* get BackendPluginsPlugin instance */;
// GET request with query params
const response = await backendPluginsPlugin.callPluginApi(
'/api/plugins/my-plugin/custom',
'GET',
{ param1: 'value1', param2: 'value2' }
);
// POST request with JSON body
const response = await backendPluginsPlugin.callPluginApi(
'/api/plugins/my-plugin/action',
'POST',
{ data: 'value' }
);
// Handle different response types
const jsonData = await response.json(); // For JSON responses
const blob = await response.blob(); // For file downloads
const text = await response.text(); // For text responses
The callPluginApi method:
- Automatically adds authentication headers (
X-Session-ID) - Handles query parameters for GET requests
- Handles JSON body for POST/PUT/etc requests
- Returns the raw
Responseobject for flexible response handling - Throws errors for non-OK responses
Key Files
- fastapi_app/lib/plugins/plugin_base.py - Base classes
- fastapi_app/lib/plugins/plugin_registry.py - Discovery
- fastapi_app/lib/plugins/plugin_manager.py - Lifecycle
- fastapi_app/routes/plugins.py - API routes
- app/src/plugins/backend-plugins.js - Frontend integration
Plugin Response Formats
Backend plugins can return results in three formats depending on the complexity and interaction requirements:
1. Inline HTML (for simple content)
Use the html field for short, simple text results that fit comfortably in the dialog:
async def execute(self, context, params: dict) -> dict:
"""Return simple HTML content."""
return {
"html": "<p>Analysis complete. Found 42 matches.</p>"
}
When to use:
- Short text results (a few paragraphs)
- Simple lists or small tables
- Quick status messages or summaries
- Content that doesn't need extensive formatting
2. Standalone Pages (for complex content)
Use the outputUrl field for complex, tabular data, or content requiring JavaScript libraries:
async def execute(self, context, params: dict) -> dict:
"""Return URL to standalone page."""
collection_id = params.get("collection")
variant = params.get("variant")
# Build URL to custom route that returns complete HTML page
view_url = f"/api/plugins/my-plugin/view?collection={collection_id}&variant={variant}"
export_url = f"/api/plugins/my-plugin/export?collection={collection_id}&variant={variant}"
return {
"outputUrl": view_url, # Displayed in iframe
"exportUrl": export_url, # Optional: enable export button
"collection": collection_id # Optional: pass data to frontend
}
When to use:
- Large tables with sorting/filtering (e.g., DataTables)
- Content requiring external JavaScript libraries
- Complex visualizations or charts
- Multi-section reports
- Any content needing custom CSS or extensive styling
Benefits:
- Proper script execution (iframe loads scripts naturally)
- Better performance (libraries load once)
- "Open in new window" button for full-screen viewing
- Cleaner separation (route generates HTML, plugin coordinates)
Implementation pattern:
- Create a custom route in
routes.pythat generates the full HTML page:
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
from fastapi_app.lib.plugins.plugin_tools import generate_datatable_page, escape_html
router = APIRouter(prefix="/api/plugins/my-plugin", tags=["my-plugin"])
@router.get("/view", response_class=HTMLResponse)
async def view_results(
collection: str = Query(...),
variant: str | None = Query(None),
session_id: str | None = Query(None),
x_session_id: str | None = Header(None, alias="X-Session-ID"),
session_manager=Depends(get_session_manager),
auth_manager=Depends(get_auth_manager),
):
"""Generate standalone HTML page with results."""
# Authenticate user (see User Authentication section)
# ...
# Prepare table data
headers = ["Column 1", "Column 2", "Column 3"]
rows = [
[escape_html("Data 1"), escape_html("Data 2"), "Data 3"],
# ... more rows
]
# Generate HTML page with DataTables
html = generate_datatable_page(
title="My Plugin Results",
headers=headers,
rows=rows,
table_id="resultsTable",
page_length=25,
default_sort_col=0,
default_sort_dir="desc",
enable_sandbox_client=True # For inter-window communication
)
return HTMLResponse(content=html)
- Return the URL from your plugin endpoint:
async def execute(self, context, params: dict) -> dict:
collection_id = params.get("collection")
view_url = f"/api/plugins/my-plugin/view?collection={collection_id}"
return {
"outputUrl": view_url,
"collection": collection_id
}
See also: edit_history plugin for complete example.
3. Preview-then-Execute Pattern (for operations requiring confirmation)
Use the outputUrl and executeUrl fields together for operations that should show a preview before execution:
async def execute(self, context, params: dict) -> dict:
"""Return URLs for preview and execute endpoints."""
collection_id = params.get("collection")
variant = params.get("variant", "all")
# Build URLs for preview and execute
variant_param = f"&variant={variant}" if variant != "all" else ""
preview_url = f"/api/plugins/my-plugin/preview?collection={collection_id}{variant_param}"
execute_url = f"/api/plugins/my-plugin/execute?collection={collection_id}{variant_param}"
return {
"outputUrl": preview_url, # Shows preview in iframe
"executeUrl": execute_url, # Execute button calls this URL
"collection": collection_id,
"variant": variant
}
When to use:
- Operations that modify data (syncing, batch updates, deletions)
- Operations where users need to review changes before applying
- Operations that can be expensive and should be confirmed
User flow:
- Plugin returns
outputUrlandexecuteUrl - Frontend displays preview in iframe (from
outputUrl) - Execute button appears in dialog footer
- User reviews changes and clicks Execute
- Execute button loads
executeUrlin the same iframe - Execute button is hidden after clicking
Implementation pattern:
Create two routes in routes.py:
@router.get("/preview", response_class=HTMLResponse)
async def preview_operation(
collection: str = Query(...),
session_id: str | None = Query(None),
x_session_id: str | None = Header(None, alias="X-Session-ID"),
session_manager=Depends(get_session_manager),
auth_manager=Depends(get_auth_manager),
):
"""Generate preview HTML (dry-run mode)."""
# Authenticate user
# Perform dry-run operation
# Generate detailed HTML with preview notice
# Return HTML with message: "Click Execute to apply these changes"
return HTMLResponse(content=preview_html)
@router.get("/execute", response_class=HTMLResponse)
async def execute_operation(
collection: str = Query(...),
session_id: str | None = Query(None),
x_session_id: str | None = Header(None, alias="X-Session-ID"),
session_manager=Depends(get_session_manager),
auth_manager=Depends(get_auth_manager),
):
"""Execute the operation and return summary HTML."""
# Authenticate user
# Perform actual operation
# Generate summary HTML (statistics only)
return HTMLResponse(content=summary_html)
Preview HTML structure:
- Include prominent notice: "Preview Mode - Click Execute to apply changes"
- Show detailed list of changes that will be made
- Use complete HTML document with styles
- Keep layout clean and readable
Execute HTML structure:
- Show success message
- Display summary statistics only (no details)
- Optionally show errors if any occurred
- Use complete HTML document with styles
See also: local_sync plugin for complete example.
4. Options-then-Execute Pattern (for user-configured operations)
Use outputUrl with sandbox.navigateIframe() when the user must configure options before execution. Unlike the Preview-then-Execute pattern (which uses a fixed executeUrl), this pattern lets the iframe itself build the next URL dynamically based on form input.
When to use:
- Operations with user-configurable parameters (mode selection, checkboxes, etc.)
- Multi-step workflows where the next URL depends on user choices
- Any case where the execute URL cannot be determined at plugin endpoint time
User flow:
- Plugin endpoint returns
outputUrlpointing to an options form route - Frontend loads options form in iframe
- User configures options and clicks "Start"
- Form JavaScript calls
sandbox.navigateIframe(url)with the built URL - Sandbox injects
session_idand navigates the iframe to the execution route - Execution route runs and displays results in the same iframe
How navigateIframe works:
The navigateIframe(url) method on the PluginSandbox class (backend-plugin-sandbox.js):
- Finds the iframe in the plugin result dialog
- Constructs a full URL from the relative path
- Injects the current
session_idfrom application state (matching the pattern used bydisplayResultInIframeandconfigureExecuteButton) - Sets
iframe.srcto navigate
Since PluginSandbox methods are auto-discovered by _extract_sandbox_methods() in plugin_tools.py, the method is automatically available as sandbox.navigateIframe() in any iframe that includes the sandbox client script.
Implementation pattern:
Step 1 — Plugin endpoint returns the options form URL:
async def update(self, context, params: dict) -> dict:
return {"outputUrl": "/api/plugins/my-plugin/options"}
Step 2 — Options route serves an HTML form with the sandbox client:
from fastapi_app.lib.plugins.plugin_tools import generate_sandbox_client_script
@router.get("/options", response_class=HTMLResponse)
async def options_form(
session_id: str | None = Query(None),
x_session_id: str | None = Header(None, alias="X-Session-ID"),
session_manager=Depends(get_session_manager),
auth_manager=Depends(get_auth_manager),
):
# Authenticate user...
sandbox_script = generate_sandbox_client_script()
html = f"""<!DOCTYPE html>
<html>
<head>
<script>{sandbox_script}</script>
</head>
<body>
<label><input type="radio" name="mode" value="a" checked> Mode A</label>
<label><input type="radio" name="mode" value="b"> Mode B</label>
<label><input type="checkbox" id="force" checked> Force overwrite</label>
<button onclick="startExecution()">Start</button>
<button onclick="sandbox.closeDialog()">Cancel</button>
<script>
function startExecution() {{
const mode = document.querySelector('input[name="mode"]:checked').value;
const force = document.getElementById('force').checked;
const params = new URLSearchParams({{ mode, force }});
sandbox.navigateIframe('/api/plugins/my-plugin/execute?' + params);
}}
</script>
</body>
</html>"""
return HTMLResponse(content=html)
Step 3 — Execute route receives the user-chosen parameters and runs the operation:
@router.get("/execute", response_class=HTMLResponse)
async def execute_operation(
mode: str = Query("a"),
force: bool = Query(False),
# ... auth params
):
# Run operation with user-selected options
# Return HTML results page
return HTMLResponse(content=results_html)
Key points:
- The options form must include the sandbox client script (
generate_sandbox_client_script()) sosandbox.navigateIframe()is available session_idis automatically injected bynavigateIframe— the form JavaScript only needs to build the path and query parameters- The "Cancel" button can call
sandbox.closeDialog()to dismiss the dialog - Each page in the chain is a full page load in the iframe, so the sandbox client reconnects on each navigation
See also: update_metadata plugin for complete example.
Interactive HTML Content
Both response formats support interactive elements through the Plugin Sandbox interface.
Plugin Sandbox API
When plugin content is displayed (either via html or outputUrl), a window.pluginSandbox object (or window.sandbox in standalone pages) exposes methods to interact with the application:
// Available methods:
// - In inline HTML: window.pluginSandbox
// - In standalone pages (outputUrl): window.sandbox
// Update application state (any fields from ApplicationState) - async
await pluginSandbox.updateState({ xml: 'doc-id', variant: 'model-x' });
// Close the result dialog
pluginSandbox.closeDialog();
// Navigate the iframe to a new URL (session_id injected automatically)
sandbox.navigateIframe('/api/plugins/my-plugin/execute?param=value');
// Convenience: Open a document (updates xml state, clears diff, closes dialog) - async
await pluginSandbox.openDocument('stable-id');
// Convenience: Open diff view (updates xml and diff states, closes dialog) - async
await pluginSandbox.openDiff('stable-id-1', 'stable-id-2');
Example: Clickable Links
In inline HTML:
async def execute(self, context, params: dict) -> dict:
"""Generate interactive HTML with clickable links."""
doc_id = "abc123"
# Create clickable link that opens document
html = f'''
<p>View document:
<a href="#"
onclick="window.pluginSandbox.openDocument('{doc_id}'); return false;"
style="color: #0066cc; text-decoration: underline;">
{doc_id}
</a>
</p>
'''
return {"html": html}
In standalone pages (outputUrl):
When using generate_datatable_page() with enable_sandbox_client=True, use sandbox (not pluginSandbox):
# In your custom route
from fastapi_app.lib.plugins.plugin_tools import escape_html
doc_link = f'''<a href="#"
onclick="sandbox.openDocument('{entry["stable_id"]}'); return false;"
style="color: #0066cc; text-decoration: underline;">
{escape_html(entry["doc_label"])}
</a>'''
rows.append([
escape_html(entry["date"]),
doc_link, # Clickable link
escape_html(entry["description"])
])
Available State Fields
The sandbox can update any field from ApplicationState (see app/src/state.js):
xml- Open XML document IDdiff- Diff XML document ID (triggers diff view when set withxml)pdf- Open PDF document IDxpath- Current XPath selectionvariant- Variant filtercollection- Current collection ID- Other fields as needed
Implementation Examples
- Inline HTML: The iaa_analyzer plugin demonstrates inline HTML with clickable stable IDs and match counts that open documents or diff views
- Standalone pages: The edit_history plugin demonstrates the
outputUrlpattern with a complete DataTables implementation
Utility Functions
The fastapi_app.lib.plugin_tools module provides utilities for generating plugin content:
generate_datatable_page() - Generate complete HTML page with sortable DataTables table:
from fastapi_app.lib.plugins.plugin_tools import generate_datatable_page, escape_html
html = generate_datatable_page(
title="Results", # Page title
headers=["Col1", "Col2"], # Column headers
rows=[ # Table rows (can contain HTML)
[escape_html("A"), "B"],
[escape_html("C"), "D"]
],
table_id="myTable", # HTML table ID
page_length=25, # Rows per page
default_sort_col=0, # Sort column index
default_sort_dir="desc", # "asc" or "desc"
enable_sandbox_client=True, # Include sandbox for links
custom_css="", # Additional CSS
custom_js="" # Additional JavaScript
)
escape_html() - Escape HTML to prevent XSS:
from fastapi_app.lib.plugins.plugin_tools import escape_html
safe_text = escape_html(user_input) # Escapes <, >, &, ", '
generate_sandbox_client_script() - Generate sandbox client for custom HTML pages (advanced use).
Notes
- Directory naming: Use underscores (e.g.,
my_plugin) not hyphens (e.g.,my-plugin) in directory names to avoid Python import issues - Shadow DOM: Frontend uses
querySelectorto access Shoelace menu elements - Plugin discovery happens at startup
- Plugins can be reloaded without restart in dev mode
- Use
PluginManager.get_instance()to access plugin manager - Plugin Sandbox: Always available as
window.pluginSandboxwhen plugin HTML is displayed - HTML escaping: Always escape user-provided content in HTML to prevent XSS attacks
Related Documentation
- Plugin System Overview - Overview of frontend and backend plugin systems
- Frontend Plugin System - Frontend plugin architecture
- Backend Plugin Development Guide - Practical guide for creating backend plugins
- Service Registry - Plugin-to-plugin communication via capability-based services
- Architecture Overview - Complete system architecture
- Access Control - RBAC implementation
- API Reference - FastAPI endpoint documentation