How the Instatic Dashboard Workspace and Widget Registry Work

The Instatic admin dashboard is a configurable 12-column tile grid whose layout state is stored in a user-level preference, while a singleton registry maps widget IDs to React components that can be contributed by both core modules and plugins.

The Instatic project (CoreBunch/Instatic) renders the admin home at /admin/dashboard as a personalized workspace. The system is built around two core abstractions: a persisted grid layout managed by useDashboardLayout.ts and a central dashboardWidgetRegistry that dynamically assembles the available tiles. Together, these mechanisms let first-party code and third-party plugins define, position, and hydrate dashboard widgets without blocking one another.

12-Column Grid Layout and State Persistence

The visual surface is provided by DashboardGrid, a CSS grid component configured with twelve columns and fixed-height rows. Each occupied cell hosts a widget that can span multiple columns and rows.

When the application boots, the grid first renders the DEFAULT_LAYOUT constant. It then overwrites that baseline with any user-specific configuration stored in the dashboard-layout site preference. This hydration logic lives in [src/admin/pages/dashboard/hooks/useDashboardLayout.ts](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/dashboard/hooks/useDashboardLayout.ts), which watches the preference and re-applies the saved positions, sizes, and order on every page reload.

Widget Registry Architecture

All runnable widgets are tracked by the singleton dashboardWidgetRegistry exported from [src/core/dashboard/registry.ts](https://github.com/CoreBunch/Instatic/blob/main/src/core/dashboard/registry.ts). The registry maintains a live map of DashboardWidgetDefinition objects keyed by their namespaced widgetId.

Every definition must supply:

  • widgetId — a namespaced string such as core.pages or myPlugin.visitors
  • component — the React component rendered inside the widget card
  • defaultSize — the initial span and rows consumed in the grid
  • iconName — a pixel-art icon key or custom icon path
  • ownerId — core for built-in widgets, or the plugin identifier for external contributions

First-Party Widget Registration

Core widgets self-register when the dashboard mounts by way of the aggregate barrel file at [src/admin/pages/dashboard/widgets/index.ts](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/dashboard/widgets/index.ts). The file exports a FIRST_PARTY_WIDGETS array that loops through local definitions and enrolls them with the registry.

For example, a built-in stats widget is defined as:

// src/admin/pages/dashboard/widgets/MyStatWidget.tsx
import { Widget } from '@ui/components/Widget';

export const MyStatWidget = ({ span, editing }) => (
  <Widget widgetId="my-stat" title="My Stats" tint="mint" span={span}>
    {/* widget content */}
  </Widget>
);
// src/admin/pages/dashboard/widgets/index.ts
import { MyStatWidget } from './MyStatWidget';

export const FIRST_PARTY_WIDGETS = [
  {
    widgetId: 'my-stat',
    component: MyStatWidget,
    defaultSize: { span: 2, rows: 2 },
    iconName: 'chart-bar',
    ownerId: 'core',
  },
  // …other widgets
];

Plugin Widget Registration via the Dashboard SDK

Plugins running inside the admin React process inject widgets through the Dashboard SDK. The concrete API is api.dashboard.widgets.register, which requires the caller to hold the dashboard.widgets.register capability.

A typical plugin registration looks like this:

// plugin admin entrypoint (runs in the admin React process)
api.dashboard.widgets.register({
  widgetId: `${pluginId}.visitors`,
  component: VisitorsWidget,
  defaultSize: { span: 3, rows: 2 },
  iconName: 'user-group',
  ownerId: pluginId,
});

As documented in the plugin-system.md spec, the namespaced widgetId prevents collisions between core and plugin tiles.

Customization Mode and Drag-and-Drop

When an admin clicks the customize toolbar button, the workspace enters an editing state. Unused widgets surface in a bottom-docked Block Library rendered by BlockLibrary.tsx.

Draggable Sources and Valid Drop Targets

Both grid cells and library tiles act as draggable sources. @dnd-kit identifies them by structured IDs such as widget:<widgetId> or library:<widgetId>. Users can move an existing tile, pull a new widget from the library into the grid, or return an active tile to the library to remove it.

Drop Validation and Ghost Behavior

The drag ghost is rendered only when the pointer hovers over a valid drop target. If the cursor overlaps an occupied or invalid cell, the target resolves to null, and the drop is prevented. This guards the 12-column invariant and stops widgets from colliding during layout edits.

Data-Backed Widgets and Permission Gating

Widgets that surface live metrics—pages, storage, AI usage, activity, and so on—own isolated data hooks rather than sharing a global fetcher.

Domain-Specific Data Hooks

Each domain exposes a dedicated endpoint under /admin/api/cms/dashboard/<domain>. The generic apiRequest client issues the call, and the response is validated through a TypeBox schema inside the hook. For instance, usePagesStats (part of the broader useDashboardStats family) consumes the pages domain endpoint and returns a typed payload.

A concrete consumption pattern is visible in widgets such as PagesWidget:

// src/admin/pages/dashboard/widgets/PagesWidget.tsx
import { usePagesStats } from '../hooks/useDashboardStats';

export const PagesWidget = ({ span }) => {
  const { data, loading } = usePagesStats();
  return (
    <Widget widgetId="pages" title="Pages" tint="sky" span={span}>
      {loading ? <SkeletonBlock /> : <StatValue>{data?.total}</StatValue>}
    </Widget>
  );
};

While loading, the widget renders a skeleton; on error, it falls back to an empty state so one slow tile cannot block the rest of the Instatic dashboard workspace.

API-Level Capability Checks

Notably, the DashboardWidgetDefinition does not embed a capability list. Authorization happens at the network boundary: the handleDashboardRoutes request handler invokes requireCapability before returning any payload. If the user lacks the required permission, the widget hook receives an empty or skeleton response, keeping sensitive data out of the UI without burdening the registry with role logic.

Summary

  • The Instatic dashboard workspace is a 12-column CSS grid whose layout state is the single source of truth persisted in the dashboard-layout user preference.
  • First-party widgets register through the barrel file at src/admin/pages/dashboard/widgets/index.ts, while plugins call api.dashboard.widgets.register after obtaining the correct capability.
  • The dashboardWidgetRegistry singleton in src/core/dashboard/registry.ts stores every DashboardWidgetDefinition by its namespaced widgetId.
  • Customization mode leverages @dnd-kit for moves, additions, and removals, with strict drop validation to prevent overlapping tiles.
  • Data hooks fetch per-domain metrics independently and render skeleton states, and the API layer guards sensitive payloads via requireCapability rather than widget-level ACLs.

Frequently Asked Questions

How is the default dashboard layout restored on login?

The useDashboardLayout hook initially applies the hard-coded DEFAULT_LAYOUT and then merges any saved user preference named dashboard-layout from the site_preferences table. If the admin has never customized the grid, the fallback baseline remains visible.

Can a plugin add widgets to the Instatic dashboard without modifying core code?

Yes. Plugins use the Dashboard SDK method api.dashboard.widgets.register inside their admin entry point, provided they declare or are granted the dashboard.widgets.register permission. The registry accepts the plugin’s ownerId and widgetId without changes to src/core/dashboard/registry.ts.

What prevents widgets from overlapping during drag-and-drop customization?

The @dnd-kit integration computes whether the dragged item’s target coordinates are valid. If the destination overlaps an occupied cell or lies outside the 12-column bounds, the drop target becomes null, the ghost disappears, and the drop action is cancelled.

Where does the dashboard store each widget’s live data?

Individual widgets call their own hooks—such as usePagesStats or useDashboardStats—which hit per-domain routes like /admin/api/cms/dashboard/<domain>. The layout registry and the grid itself never manage or cache metric data; that responsibility stays isolated within each widget’s data layer.

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 →