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.json without touching core code, as processed by src/admin/plugin-host-hooks/pluginContext.ts.
  • Global registry: Widget definitions are stored in an in-memory registry queried by src/ui/components/WidgetList/WidgetList.tsx at runtime.
  • Consistent UI: The Widget wrapper in src/ui/components/Widget/Widget.tsx provides uniform styling and layout for all dashboard elements.
  • Sandboxed execution: QuickJS-WASM isolation via server/plugins/quickjs/bootstrap ensures 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →