Skip to content

Documentation

Dashboard Widget Architecture

Technical deep-dive into dashboard widget packages β€” folder structure, widget.json manifest, dual registration, and licensing gates.

8/23/2026
5 min read Edit on GitHub

1. Package Model

Every dashboard widget is a Portable Module: a self-contained folder that can be shipped, uploaded to marketplace.sveltycms.com, and installed into any SveltyCMS installation without touching dashboard core code.

widgets/<kebab-folder>/
β”œβ”€β”€ <component>.svelte    # Required β€” the widget component
β”œβ”€β”€ widget.json           # Required β€” marketplace/telemetry manifest
β”œβ”€β”€ <component>.mdx       # Required β€” marketplace description (features, licensing, data source)
└── tests/                # Optional β€” unit tests

Why a folder per widget?

  • Marketplace uploads β€” the marketplace API accepts package folders directly; widget.json is the package manifest and the co-located <component>.mdx is the package description used for the marketplace listing.
  • Clean installs β€” installDashboardWidget() writes the whole folder via the package installPath, no file-by-file merging.
  • Stable registry keys β€” the folder id (widget.json.id) is the key persisted in system-preferences. The entry filename is always index.svelte; renaming the implementation file never resets user layouts.

2. The widget.json Manifest

{
  "id": "system-health",
  "name": "System Health",
  "description": "Monitor system services and overall health",
  "icon": "mdi:heart-pulse",
  "version": "1.0.0",
  "type": "dashboard-widget",
  "author": "SveltyCMS",
  "license": "free",
  "price": 0,
  "component": "index",
  "defaultSize": { "w": 2, "h": 2 },
  "category": "monitoring",
  "sveltycms": ">=0.0.8"
}
Field Type Description
id string Kebab-case folder name (package id).
name string Display name shown in the picker / marketplace.
description string Short description for cards and listings.
icon string Iconify icon id.
version string Semver package version.
type "dashboard-widget" Fixed type discriminator.
author string Publisher name.
license "free" \| "freemium" \| "paid" Monetization model.
price number EUR price (0 for free).
component string Entry filename without .svelte (always "index").
defaultSize { w: 1-4, h: 1-4 } Recommended grid size.
category "monitoring" \| "logs" \| "content" \| "static" Default fetch/cache/refresh behavior group.
requiresPlugin string (optional) Picker omits the package unless this plugin is on.

The manifest must agree with the component’s widgetMeta export (name, icon, description, defaultSize). manifest-registry.ts reads manifests at build time via import.meta.glob("./*/widget.json", { eager: true }).

3. Dual Registration

Widgets are registered in two places, both compile-time β€” Svelte modules are not evaluated to build the picker:

  1. Server metadata β€” +page.server.ts reads getInstalledDashboardWidgets() (widget.json glob) and maps picker entries via manifestsToPickerList(). Packages whose sveltycms range the host does not satisfy are omitted. The same load hydrates the saved layout from system.preferences so the client skips a /api/system-preferences round-trip.

  2. Client runtime β€” +page.svelte builds the picker from data.availableWidgets (JSON). Widgets are optional: none ship on an empty dashboard. import.meta.glob("./widgets/*/*.svelte") is a lazy chunk map only β€” a package is imported when the user adds it or its tile intersects the viewport. Packages with requiresPlugin stay out of the picker until that plugin is enabled (Commerce Orders / Inventory β†’ commerce, off by default). The registry is keyed by the package folder id (widget.json.id).

The folder id is the stable key β€” widget.json.id matches the folder, layouts persist that id, and the entry filename (index.svelte) is an implementation detail that can never break a saved layout. In dev, the Vite plugin (sveltyCmsPlugin) watches src/routes/(app)/dashboard/widgets/** and triggers a full reload on package add/remove β€” new widgets appear in the picker without a manual restart (same treatment as src/widgets).

Polling widgets pause while document.hidden is true and refetch when the tab is visible again if the poll interval has elapsed. Widget fetches use cache: "no-store" instead of a _=Date.now() query string.

4. Base Widget Contract

All widgets compose base-widget.svelte, which provides fetch/poll/retry/cache/refresh chrome, the header, resize handles, and keyboard-accessible controls:

<BaseWidget
  {label} {theme} {icon} {widgetId} {size}
  endpoint="/api/dashboard/health"
  pollInterval={5000}
  onSizeChange={onSizeChange}
  onCloseRequest={onRemove}
>
  {#snippet children({ data, error, isLoading, refresh })}
    <!-- render data -->
  {/snippet}
</BaseWidget>

The children snippet receives { data, updateWidgetState, getWidgetState, refresh, isLoading, error }.

5. Component Metadata (widgetMeta)

Each component still exports module-level metadata (keep it in sync with widget.json). The Add Widget picker reads widget.json only so Svelte modules stay lazy:

<script lang="ts" module>
  export const widgetMeta = {
    name: "System Health",
    icon: "mdi:heart-pulse",
    description: "Monitor system services and overall health",
    defaultSize: { w: 2, h: 2 },
  };
</script>

widget.json drives the picker, packaging, catalog, and telemetry. widgetMeta remains on the component for local documentation β€” keep the two in sync.

6. πŸ”‘ Licensing Gates

Dashboard widgets use the same license model as content widgets and plugins:

Model Behavior
Free No checks; bundled or community packages.
Freemium 14-day key-less trial; premium features gate on checkExtensionLicense.
Paid License required from install; widget renders an upgrade prompt otherwise.
  • Client-side gating β€” checkExtensionLicense("dashboard", widgetId); on failure render <UpgradePrompt extensionId="dashboard:<widgetId>" price="…" /> instead of content.
  • Server-side gating β€” premium dashboard endpoints are gated in src/routes/api/[...path]/handlers/dashboard-license.ts (requireDashboardWidgetLicense), which calls checkExtensionLicense("dashboard", widgetId) and returns 403 LICENSE_REQUIRED when the trial expired without a license. The endpoint β†’ widget-id map (DASHBOARD_ENDPOINT_LICENSE) covers /api/dashboard/{audit, logs, security, scim, cache-metrics, online-user, metrics, commerce-orders, commerce-inventory} and /api/database/pool-diagnostics. Free widgets are not gated. When you add a premium widget backed by a new endpoint, register it in that map.

Example (cache-monitor/index.svelte):

{#if licenseStatus && !licenseStatus.active && !licenseStatus.hasLicense}
  <!-- Upgrade prompt -->
{:else}
  <BaseWidget endpoint="/api/dashboard/cache-metrics" ...>…</BaseWidget>
{/if}

7. Installed-Package Inventory

manifest-registry.ts exposes the installed inventory for consumers that must not compile Svelte:

import { getInstalledDashboardWidgets } from "./widgets/manifest-registry";

const packages = getInstalledDashboardWidgets(); // sorted by name
// β†’ [{ id: "system-health", name: "System Health", license: "free", … }]

Used by the marketplace catalog (marketplace-service.ts) and telemetry (telemetry-service.ts).

Related

dashboardwidgetsarchitecturedevelopment
Was this page helpful?