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 ascore.pagesormyPlugin.visitorscomponent— the React component rendered inside the widget carddefaultSize— the initialspanandrowsconsumed in the gridiconName— a pixel-art icon key or custom icon pathownerId—corefor 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-layoutuser preference. - First-party widgets register through the barrel file at
src/admin/pages/dashboard/widgets/index.ts, while plugins callapi.dashboard.widgets.registerafter obtaining the correct capability. - The
dashboardWidgetRegistrysingleton insrc/core/dashboard/registry.tsstores everyDashboardWidgetDefinitionby its namespacedwidgetId. - Customization mode leverages
@dnd-kitfor 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
requireCapabilityrather 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →