Dashboard Widget Architecture
Technical deep-dive into dashboard widget packages β folder structure, widget.json manifest, dual registration, and licensing gates.
On this page
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.jsonis the package manifest and the co-located<component>.mdxis the package description used for the marketplace listing. - Clean installs β
installDashboardWidget()writes the whole folder via the packageinstallPath, no file-by-file merging. - Stable registry keys β the component filename (not the folder) is the key persisted in
system-preferences, so upgrading or renaming a package does not reset 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": "system-health-widget",
"defaultSize": { "w": 2, "h": 2 },
"category": "monitoring"
}
| 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 |
Component filename without .svelte (registry key). |
defaultSize |
{ w: 1-4, h: 1-4 } |
Recommended grid size. |
category |
"monitoring" \| "logs" \| "content" \| "static" |
Default fetch/cache/refresh behavior group. |
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:
- Client runtime β
+page.svelteglobs./widgets/*/*.svelteto build the lazy-loading registry. The registry is keyed by the package folder id (widget.json.id); each entry carries the entry filename (index.svelte) for the dynamic import:
// +page.svelte β path like "./widgets/system-health/index.svelte"
const segments = path.split("/");
const folder = segments[segments.length - 2];
const entryFile = segments[segments.length - 1]?.replace(".svelte", "");
registry[folder] = { component: module.default, entryFile, folder, ... };
- Server metadata β
+page.server.tseagerly globs the same pattern and pre-computesavailableWidgets(name, icon, description, folder) for the widget picker dropdown.
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).
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 exports module-level metadata that the picker and registry read directly:
<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>
widgetMeta drives the UI; widget.json drives packaging, catalog, and telemetry. Keep them 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 callscheckExtensionLicense("dashboard", widgetId)and returns403 LICENSE_REQUIREDwhen 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}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).