# How Instatic's Dashboard Widget System Supports Plugin Widgets

> Discover how Instatic's dashboard widget system easily supports plugin widgets. Learn how plugins register and render custom dashboard elements for a seamless user experience.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-26

---

**Instatic enables third-party plugins to inject custom dashboard widgets by reading widget definitions from each plugin's [`plugin.json`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) file, as documented in [`docs/features/plugin-system.md`](https://github.com/CoreBunch/Instatic/blob/main/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.

```json
{
  "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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```tsx
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.

```tsx
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`](https://github.com/CoreBunch/Instatic/blob/main/src/ui/components/Widget/Widget.tsx). This wrapper enforces consistent styling, header formatting, and responsive behavior through a shared CSS module.

```tsx
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`](https://github.com/CoreBunch/Instatic/blob/main/plugin.json) without touching core code, as processed by [`src/admin/plugin-host-hooks/pluginContext.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/ui/components/WidgetList/WidgetList.tsx) at runtime.
- **Consistent UI**: The `Widget` wrapper in [`src/ui/components/Widget/Widget.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.