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:

  1. The new version record is created with is_active = true
  2. All previous versions are set to is_active = false
  3. 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:

  1. Cascade-deletes the dashboard record, all versions, and access grants from the registry
  2. Removes bundle files from disk
  3. 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.