Dashboard Publishing
Dashboard Publishing¶
This document defines the end-to-end pipeline for publishing a dashboard: the build contract that dashboard projects must satisfy, the bundle format uploaded to the registry, the CLI commands that drive the workflow, and the validation rules enforced at each step.
Overview¶
Publishing a dashboard is a three-step process:
Developer's machine Huitzo Registry
┌──────────────────────────────────┐ ┌─────────────────────────┐
│ 1. Build │ │ │
│ huitzo dashboard build │ │ │
│ → npm run build (lib mode) │ │ │
│ → dist/main.js (self-contained) │ │
│ │ │ │
│ 2. Validate │ │ │
│ huitzo dashboard validate │ │ │
│ → Check manifest + bundle │ │ │
│ │ │ │
│ 3. Publish │ │ │
│ huitzo dashboard publish │ │ │
│ → Create .tar.gz from dist/ │────────▶│ Extract, store, register│
│ → Upload to registry │ │ Serve at /d/{slug} │
└──────────────────────────────────┘ └─────────────────────────┘
Build Contract¶
Dashboards are built as self-contained ES module libraries. Each dashboard bundles all of its dependencies — including React, ReactDOM, and the Huitzo Dashboard SDK — so that it has no version coupling with Hub.
Why Dashboards Bundle Everything¶
Hub is a platform that hosts third-party dashboards. If dashboards shared React with Hub, a Hub upgrade (e.g., React 19 → 20) could break every third-party dashboard that hasn't updated. By bundling everything, dashboards are fully independent:
| Shared React (rejected) | Bundled React (current) |
|---|---|
| Hub upgrade breaks third-party dashboards | No impact — dashboards use their own version |
| Developers must track Hub's React version | Free to use any React version |
| ~200 KB smaller per dashboard | ~200 KB larger (acceptable tradeoff for independence) |
See Loading Architecture — Why Dashboards Bundle Their Own React for the full rationale.
Vite Library Mode¶
The dashboard's vite.config.ts must use library mode to produce an ESM module with mount/unmount exports:
// vite.config.ts — dashboard project build configuration
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js';
export default defineConfig(({ mode }) => ({
plugins: [react(), cssInjectedByJsPlugin()],
define: {
'process.env.NODE_ENV': JSON.stringify(mode),
'process.env': '{}',
},
build: {
lib: {
entry: 'src/main.tsx',
formats: ['es'],
fileName: 'main',
},
cssCodeSplit: false,
},
}));
| Option | Value | Why |
|---|---|---|
lib.entry |
src/main.tsx |
Dashboard entry point — must export mount and unmount |
lib.formats |
['es'] |
ESM only — Hub uses import() to load modules |
lib.fileName |
'main' |
Convention: output is always dist/main.js |
cssCodeSplit |
false |
All CSS concatenated into one output — no per-chunk splitting |
define |
process.env.* |
Replaces Node.js globals at build time — required because Vite library mode does not auto-replace them, and React checks process.env.NODE_ENV internally |
cssInjectedByJsPlugin |
plugin | Injects CSS as <style> tags when the JS module loads — required because Vite library mode emits a separate CSS file by default, but Hub only loads main.js |
No external configuration is needed — all dependencies (React, SDK, etc.) are bundled into the output.
CSS Handling¶
CSS is injected into the JavaScript module — there is no separate CSS file to load. Vite's cssCodeSplit: false combined with library mode injects styles via <style> tags when the module is imported.
This means: - Hub loads one file per dashboard (the JS module) - Styles are automatically applied on import - CSS Modules are still required to prevent style collisions between dashboards
Entry Point Contract¶
The dashboard entry point (src/main.tsx) must satisfy the Module Contract — export mount and unmount functions:
// src/main.tsx — must export mount and unmount
import { createRoot, type Root } from 'react-dom/client';
import type { HuitzoContext } from '@huitzo/dashboard-sdk';
import { HuitzoProvider } from '@huitzo/dashboard-sdk-react';
import { ErrorBoundary } from './components/ErrorBoundary';
import App from './App';
let root: Root | null = null;
export function mount(container: HTMLElement, context: HuitzoContext): void {
root = createRoot(container);
root.render(
<ErrorBoundary fallback={<CrashScreen onReturn={() => context.navigateToHub()} />}>
<HuitzoProvider context={context}>
<App />
</HuitzoProvider>
</ErrorBoundary>
);
}
export function unmount(_container: HTMLElement): void {
root?.unmount();
root = null;
}
Hub calls mount(container, context) when the user navigates to the dashboard and unmount(container) when they navigate away. The dashboard creates its own React root inside the provided container.
Bundle Format¶
Build Output¶
After huitzo dashboard build (or npm run build), the dist/ directory contains:
dist/
├── main.js # Self-contained ESM module (React + SDK + app code + CSS)
└── assets/ # (optional) static assets (images, fonts)
├── logo-abc123.png
└── chart-data.json
| File | Required | Description |
|---|---|---|
main.js |
Yes | Self-contained ESM module exporting mount and unmount |
assets/* |
No | Static assets referenced by the dashboard |
Upload Artifact¶
The CLI packages dist/ into a gzipped tarball for upload:
claims-dashboard-1.2.0.tar.gz
└── main.js
└── assets/
└── ...
Size & Content Limits¶
| Constraint | Limit |
|---|---|
| Maximum tarball size | 50 MB |
| Maximum files | 500 |
| Allowed file types | .js, .css, .json, .png, .jpg, .svg, .woff, .woff2, .ttf |
CLI Workflow¶
The dashboard CLI commands (huitzo dashboard validate, build, publish) are documented in the CLI Reference — Dashboard Commands.
Validation Rules¶
Manifest Validation¶
| Rule | Check |
|---|---|
| Required fields present | dashboard.name, dashboard.namespace, dashboard.version, dashboard.description |
| Name format | kebab-case, 3-50 characters, ^[a-z][a-z0-9-]*[a-z0-9]$ |
| Namespace format | Lowercase, no dashes, 2-20 characters |
| Version format | Valid semver (MAJOR.MINOR.PATCH) |
| Description length | 10-200 characters |
| Visibility value | One of: public, unlisted, organization, private |
| min_sdk_version | Valid semver, must be a released SDK version |
Bundle Validation¶
| Rule | Check |
|---|---|
| Entry point exists | dist/main.js must exist (convention), validated against build.entry_point in manifest |
| Entry point is ESM | File is a valid ES module |
Exports mount |
Named export mount must be present |
Exports unmount |
Named export unmount must be present |
| Size limit | Tarball ≤ 50 MB |
| File count | ≤ 500 files |
| Allowed types | Only permitted file extensions (see Size & Content Limits) |
Pack Dependency Validation (Online)¶
When connected to the registry, the CLI also checks:
| Rule | Check |
|---|---|
| Required packs exist | All required: true dependencies are published in the registry |
| Version ranges valid | Semver ranges parse correctly |
| Scope format | Starts with @, e.g., @acme |
Version Management¶
Semantic Versioning¶
Dashboard versions follow semver:
MAJOR.MINOR.PATCH
│ │ │
│ │ └── Bug fixes, visual tweaks
│ └───────── New features, added pages
└──────────────── Breaking changes to pack dependencies or data format
Active Version¶
Each dashboard has exactly one active version at a time. When a new version is published:
- The new version record is created with
is_active = true - All previous versions are set to
is_active = false - Hub immediately serves the new active version to all users
There is no gradual rollout — the switch is immediate.
Rollback¶
To roll back, publish the previous code with a new patch version:
# If 1.3.0 is broken, check out the 1.2.0 code and publish as 1.3.1
git checkout v1.2.0
# bump version to 1.3.1 in huitzo-dashboard.yaml
huitzo dashboard publish
Deleting a Dashboard¶
To permanently remove a dashboard and all its versions:
huitzo dashboard delete {slug}
This performs:
- Cascade-deletes the dashboard record, all versions, and access grants from the registry
- Removes bundle files from disk
- The dashboard is immediately inaccessible — any cached references in Hub will return 404
This action is irreversible. To temporarily hide a dashboard without deleting it, change its visibility to private:
# pseudocode — hide instead of delete
huitzo dashboard update {slug} --visibility private
See REST API — Delete Dashboard for the API details.
Related Documentation¶
- Dashboard Framework Overview — What dashboards are and how to build them
- Dashboard Manifest Reference — Complete
huitzo-dashboard.yamlspecification - Dashboard Loading Architecture — How Hub discovers and renders dashboard modules at runtime
- REST API — Dashboards — Dashboard API endpoints
- CLI Reference — Complete CLI command reference
- Private Registry & Access Control — Pack access control model (same pattern)