How Instatic's Dashboard Widget System Supports Plugin Widgets
Instatic enables third-party plugins to inject custom dashboard widgets by reading widget definitions from each plugin's plugin.json manifest, registering them in a global registry, and rendering them inside a standardized Widget wrapper component that handles styling and layout.
Instatic's dashboard architecture separates presentation from plugin logic, allowing external developers to extend the admin interface without modifying core UI code. The system defined in the CoreBunch/Instatic repository uses a manifest-driven approach where widgets are declared as JSON metadata and loaded dynamically at runtime. This design ensures that plugin-provided widgets integrate seamlessly with core widgets while maintaining strict security boundaries.
Widget Registration Architecture
Declaring Widgets in Plugin Manifests
Plugins expose dashboard widgets through a declarative widgets array inside their plugin.json file, as documented in docs/features/plugin-system.md. Each widget definition requires a unique identifier, display title, icon reference, and the relative path to its React component implementation.
{
"name": "my-awesome-stats",
"apiVersion": "1.0",
"widgets": [
{
"id": "awesome-stats",
"title": "Awesome Stats",
"icon": "chart-bar",
"component": "./widgets/AwesomeStatsWidget.tsx"
}
],
"permissions": ["read:site", "write:site"]
}
Registry Initialization and Validation
When the Instatic server boots, the plugin loader reads these manifests and validates them against TypeBox schemas. The registration logic in src/admin/plugin-host-hooks/pluginContext.ts processes each widget definition and populates an in-memory global registry accessible to the dashboard UI.
The QuickJS bootstrap sequence (server/plugins/quickjs/bootstrap/src/...) performs the actual sandbox initialization, ensuring widget components are loaded within an isolated execution context before their metadata reaches the registry.
Rendering Pipeline
Dynamic Widget Loading
The dashboard UI entry point at src/ui/components/WidgetList/WidgetList.tsx queries the global registry via getRegisteredWidgets() and iterates over the returned definitions. It renders each entry inside the core Widget wrapper while dynamically importing the plugin-specific component paths.
import { useEffect, useState } from 'react';
import { Widget } from '../Widget';
import { getRegisteredWidgets } from '@core/plugin-registry';
export function WidgetList() {
const [widgets, setWidgets] = useState<RegisteredWidget[]>([]);
useEffect(() => {
// Load widget definitions from the plugin registry
setWidgets(getRegisteredWidgets());
}, []);
return (
<div className="dashboard-grid">
{widgets.map(({ id, title, icon, componentPath }) => (
<Widget key={id} title={title} icon={icon}>
{/* Dynamically import the plugin's component */}
<LazyComponent path={componentPath} />
</Widget>
))}
</div>
);
}
Lazy Component Isolation
The LazyComponent helper manages asynchronous imports through the plugin runtime's importPluginComponent function. This ensures widget code executes within the QuickJS-WASM sandbox, preventing unauthorized access to Node.js APIs or the host system.
import { useState, useEffect } from 'react';
export function LazyComponent({ path }: { path: string }) {
const [Component, setComponent] = useState<React.ComponentType | null>(null);
useEffect(() => {
// `importPluginComponent` is provided by the plugin runtime
importPluginComponent(path).then(setComponent);
}, [path]);
return Component ? <Component /> : null;
}
Core UI Components
Standardized Widget Wrapper
All dashboard widgets—whether core or plugin-provided—render inside the Widget component defined in src/ui/components/Widget/Widget.tsx. This wrapper enforces consistent styling, header formatting, and responsive behavior through a shared CSS module.
import { cn } from '@ui/cn';
import styles from './Widget.module.css';
export function Widget({ title, icon, children }: Props) {
return (
<section className={cn(styles.widget, 'dashboard-widget')}>
<header className={styles.header}>
<i className={cn('icon', `icon-${icon}`)} />
<h3>{title}</h3>
</header>
<div className={styles.body}>{children}</div>
</section>
);
}
Using this abstraction, plugin authors need only implement the internal logic of their widget; the framework handles visual framing and dashboard grid placement automatically.
Summary
- Manifest-driven registration: Plugins declare widgets in
plugin.jsonwithout touching core code, as processed bysrc/admin/plugin-host-hooks/pluginContext.ts. - Global registry: Widget definitions are stored in an in-memory registry queried by
src/ui/components/WidgetList/WidgetList.tsxat runtime. - Consistent UI: The
Widgetwrapper insrc/ui/components/Widget/Widget.tsxprovides uniform styling and layout for all dashboard elements. - Sandboxed execution: QuickJS-WASM isolation via
server/plugins/quickjs/bootstrapensures plugin widgets run with restricted permissions. - Dynamic imports: Components load on-demand through
importPluginComponent, keeping the initial bundle size small while supporting plugin extensibility.
Frequently Asked Questions
How do I register a custom widget in an Instatic plugin?
Add a widgets array to your plugin's plugin.json manifest file. Each object requires an id, title, icon, and component path pointing to your React component. When the server starts, the plugin loader automatically registers these definitions in the global widget registry.
What security measures protect the dashboard from malicious plugin widgets?
Instatic executes all plugin widget code inside a QuickJS-WASM sandbox that runs within the bootstrap layer at server/plugins/quickjs/bootstrap. This isolates plugin logic from the host Node.js process and enforces permission boundaries defined in the manifest, preventing unauthorized file system or network access.
Can plugin widgets access core Instatic APIs?
Yes, but only through the restricted interface exposed by the plugin runtime. Widgets use the importPluginComponent function to load their React components, which provides controlled access to approved APIs. Direct imports of core modules are blocked by the sandbox environment.
Where is the widget registry stored during runtime?
The widget registry exists as an in-memory data structure initialized during server startup by src/admin/plugin-host-hooks/pluginContext.ts. It is not persisted to disk; instead, it is rebuilt on every boot by scanning installed plugin manifests and validating them with TypeBox schemas.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →