Custom Widgets
Author custom dashboard widgets with React + Zod, and install them into ServerBee as a single file or a zip collection.
ServerBee dashboards natively support custom widget modules. Each widget is a standalone ES module that an admin installs once and can then drop onto any dashboard, exactly like a built-in widget. This guide covers the two supported install methods:
- Method B — a single
.jsfile (one widget per file) - Method C — a
.zipcollection bundle (multiple widgets in one upload, sharing the same build output)
Concepts
A widget module has three parts:
- Static JSDoc manifest — a
@serverbee-widget {...}JSON block at the top of the file declaring the widget'sid,version,name,category, default sizing, required SDK version, and so on. The manifest is statically parseable: the server can index it without ever executing the module, which means offline scanning and permission checks work the same way as runtime loading. - Default export — the object returned by
defineWidget({ configSchema, component, actions? }).configSchemais a Zod schema describing the configurable fields;componentis a React component that receives{ config, size, isEditing, actions }props at runtime. - Runtime dependencies — imported as
react,react/jsx-runtime, and@serverbee/widget-sdk. These are provided by the host and must be markedexternalat build time; the browser reuses the main app's instances.
Trust model
Custom widgets are admin-only to install and run same-origin in the browser, sharing the session of the logged-in user. That means:
- An approved widget can issue any API request that the current user is authorized for.
- All widgets share the same React context, so do not introduce global side effects that could break sibling widgets.
- The platform does not sandbox widget code — only install code you trust.
Method B — single .widget.js file
Anatomy
/**
* @serverbee-widget {
* "id": "com.example.hello",
* "version": "1.0.0",
* "name": "Hello",
* "description": "A minimal example widget.",
* "author": "Your Name",
* "category": "Real-time",
* "sizing": { "defaultW": 3, "defaultH": 2, "minW": 2, "minH": 2, "strategy": "free" },
* "sdkVersion": "^0.1.0"
* }
*/
import { defineWidget, useServers, useTheme, z } from '@serverbee/widget-sdk'
const ConfigSchema = z.object({
greeting: z.string().describe('Greeting text').default('Hello, ServerBee')
})
export default defineWidget({
configSchema: ConfigSchema,
component: ({ config }) => {
const servers = useServers()
const theme = useTheme()
const online = servers.filter((s) => s.online).length
const { greeting } = config
return (
<div style={{ padding: 12 }}>
<div style={{ fontSize: 16, fontWeight: 600 }}>{greeting}</div>
<div style={{ marginTop: 8, color: 'var(--muted-foreground)' }}>
{online} / {servers.length} online · {theme.mode} mode
</div>
</div>
)
}
})Manifest fields
| Field | Required | Notes |
|---|---|---|
id | yes | Reverse-DNS-style unique identifier, e.g. com.example.cpu |
version | yes | Semantic version MAJOR.MINOR.PATCH |
name | yes | Display name in the widget picker |
description | – | Free-form description |
author | – | Author name or organization |
category | yes | One of Real-time, Charts, Status |
sizing.defaultW/H | yes | Default width / height in grid cells |
sizing.minW/H | yes | Minimum allowed size |
sizing.maxW/H | – | Optional maximum size |
sizing.strategy | yes | free / fixed / aspect-square / content-height |
sdkVersion | yes | SDK version range, e.g. ^0.1.0 |
Build requirements
A custom widget must be built as an ES module with the following dependencies marked external:
reactreact/jsx-runtime@serverbee/widget-sdk
Just as important: the JSDoc manifest comment must be preserved verbatim at the top of the output. Most minifiers strip comments by default, so explicitly opt into preserving the manifest comment.
A typical Vite + Terser config:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
lib: {
entry: 'src/index.tsx',
formats: ['es'],
fileName: () => 'index.js'
},
rollupOptions: {
external: ['react', 'react/jsx-runtime', '@serverbee/widget-sdk']
},
minify: 'terser',
terserOptions: {
format: {
// Keep the @serverbee-widget manifest comment.
comments: /@serverbee-widget/
}
}
}
})Install via UI
- Sign in as an admin.
- Open Settings → Widget Modules.
- Choose Upload
.jsto upload a local file, or Import URL to fetch over HTTPS. - Once installed, open any dashboard, click Edit → Add Widget, and the new widget appears in the picker.
Install via API
# Upload a local file
curl -X POST "https://your-host/api/widget-modules" \
-H "X-API-Key: $SERVERBEE_API_KEY" \
-F file=@my.widget.js
# Fetch from URL
curl -X POST "https://your-host/api/widget-modules?url=https://cdn.example.com/my.widget.js" \
-H "X-API-Key: $SERVERBEE_API_KEY"On success:
{ "data": { "id": "com.example.hello", "version": "1.0.0" } }If a widget with the same id already exists, it is upgraded in place — the version, manifest, and code are all replaced.
Method C — .zip collection bundle
A collection lets you install multiple widgets in a single upload. Each widget still follows the Method B rules (its own JSDoc manifest, its own entry file). They are simply packaged together in one .zip, and a root-level collection.json enumerates them.
Package layout
my-pack.zip
├── collection.json
├── weather/
│ ├── index.js ← contains the @serverbee-widget manifest
│ └── icon.svg ← optional asset
├── clock/
│ └── index.js
└── shared/
└── helpers.js ← optional; not auto-injected — entry files must import as neededcollection.json schema
{
"widgets": [
{ "entry": "weather/index.js" },
{ "entry": "clock/index.js" }
]
}Constraints:
entrymust be a relative path (no leading/, no..).- Must point to a
.jsor.mjsfile. - Every entry file must contain a valid
@serverbee-widgetJSDoc block. - Widget
ids must be unique within the bundle — duplicates are rejected.
Asset resolution
Assets inside the bundle (images, JSON, helper scripts, etc.) are served via GET /api/widget-modules/{id}/{relative-path}. The relative path is resolved against the folder of the entry file:
| Request URL | Resolved zip path |
|---|---|
/api/widget-modules/com.example.weather/index.js | weather/index.js |
/api/widget-modules/com.example.weather/icon.svg | weather/icon.svg |
/api/widget-modules/com.example.clock/index.js | clock/index.js |
Reference assets from widget code like this:
const iconUrl = `/api/widget-modules/com.example.weather/icon.svg`Note: widgets cannot reach into each other's folders — asset resolution is scoped per id.
Build and pack
- Use your bundler of choice (Vite, tsup, esbuild, ...) to build each widget's
index.js, externalizing React and the SDK and preserving the manifest comment. - Lay the build outputs out in the directory structure above.
- Write
collection.jsonat the package root. - Zip it:
zip -r my-pack.zip collection.json weather/ clock/Install
The UI flow is identical to Method B — just upload the .zip. The server sniffs the PK\x03\x04 magic bytes and dispatches automatically.
curl -X POST "https://your-host/api/widget-modules" \
-H "X-API-Key: $SERVERBEE_API_KEY" \
-F file=@my-pack.zipA collection install returns an array — one entry per widget in the bundle:
{
"data": [
{ "id": "com.example.weather", "version": "1.0.0" },
{ "id": "com.example.clock", "version": "1.0.0" }
]
}Each widget gets its own row in the database and can be enabled, disabled, or uninstalled individually. They share a single underlying zip blob for storage.
Sizing strategies
sizing.strategy controls how a widget can be resized on the dashboard:
free— user can resize freely betweenminW/HandmaxW/H.fixed— locked todefaultW/H; resizing is disabled.aspect-square— always 1:1; snaps to the nearest tier.content-height— width is resizable; height is driven by content.
See Dashboards & Widgets for more on layout and editing.
SDK surface at a glance
@serverbee/widget-sdk is the stable API exposed to custom widgets. It includes:
defineWidget— declares a widget's entry point.z/ZodSchema— bundled Zod for describing config schemas.- Live hooks:
useServers,useServer,useMetric,useCapability(subscribe to the live WebSocket store viauseSyncExternalStore). - Domain hooks:
useHistory,useTraffic,useAlerts,useServiceMonitors,useUptime,useGeoIp. - Host hooks:
useTheme,useConfigUpdate. - Generic escape hatches:
useApiQuery/useApiMutationfor direct REST calls. createActionsHelperandActionDefinitionfor registering action buttons on a widget.
See packages/widget-sdk/README.md in the repo for the complete API reference.
Uninstall
Click the delete button next to a widget in Settings → Widget Modules, or:
curl -X DELETE "https://your-host/api/widget-modules/com.example.hello" \
-H "X-API-Key: $SERVERBEE_API_KEY"Built-in widgets (for example com.serverbee.hello-world) cannot be uninstalled — DELETE against them returns 400.
Limits and safety
- Size caps: a single
.jsfile or.zipmay not exceed 1 MiB on upload; each entry inside a zip is capped at 5 MiB uncompressed; the whole zip is capped at 32 MiB total uncompressed across at most 64 entries. - SSRF protection: URL installs resolve DNS and reject any host whose IP falls in a reserved/private range — loopback (
127.0.0.0/8,::1), private networks (10/8,172.16/12,192.168/16), CGNAT (100.64/10), link-local (169.254/16— includes cloud metadata endpoints), IPv6 ULA (fc00::/7) and link-local (fe80::/10), benchmarking, documentation, multicast, and reserved ranges. HTTP redirects are disabled — 3xx responses are rejected outright so a public URL cannot pivot into the private network. - Zip-slip protection: extraction rejects entries with
..or absolute paths. - Static parsing only: the manifest must be extractable with a plain regex — there is no
eval/Function()involved in installation. The extractor itself rejects sources > 1 MiB up front. - ID conflict rejection: an upload whose
idalready belongs to a module from a different source (e.g. trying to overwrite a built-in with an upload) is refused with409 Conflict. - SDK version gating: each manifest declares
sdkVersionas a semver range; the loader refuses to register a module whose required range does not match the host SDK. - Admin-gated: install and uninstall endpoints require admin privileges; members can only list and fetch served assets.
- Audit logged: every install and uninstall is recorded in the audit log with the actor, source, id, version, and code SHA-256.
- Same-origin execution: widgets run in the same browsing context as the main SPA — only install code from sources you trust.
- Action buttons declared via
defineWidget({ actions })get a built-in confirm dialog (whenconfirmis set), pending state, and toast notifications on success/failure.