Hanabi Developer Hub
/ Get started / Developer guide
Get started

Developer guide

Build a module end to end: scaffold, develop, validate, test, package, publish, install.

Scaffold
Validate
Package
Upload
Review
Publish
Install
From your machine to every user's window.

This is the end-to-end guide for building a Hanabi module: scaffold → develop → validate → test → package → upload → review → publish → install → launch. The module host, capability bridge, worker sandbox, and Developer Portal are all live; see architecture.md §12 for the phase history.

For the contract details referenced here, see manifest-reference.md, capability-api.md, and limitations.md.

0. Mental model in one paragraph #

A module is a small web app described by a hanabi.module.json manifest. Its UI runs in a sandboxed iframe and talks to Hanabi only through a permissioned capability bridgeThe trusted layer that carries every request from your sandboxed UI to the platform, checking permissions. (pick/read/write files, queue jobs, store settings). Heavy or native work (Office, image libraries) lives in an optional server workerAn optional Python program that does heavy or native work off the browser, in a sealed container. that the platform runs for trusted modules. You build the package locally with the CLI, upload it in the Developer Portal, and an admin reviews it before it appears in the Module Store.

1. Prerequisites #

  • Node 18+ and the repo installed (npm install at the root).
  • A signed-in Hanabi account (seeded; e.g. admin/admin-dev-password in dev).
  • The module tooling workspaces: packages/module-cli, packages/module-sdk.

2. Scaffold a new module — hanabi module init #

powershell
npm --workspace packages/module-cli run hanabi -- module init "My Module" --dir .\my-module

This creates a starter package:

text
my-module/
  hanabi.module.json        # manifest, prefilled with safe defaults
  ui/index.html             # posts HANABI_READY on load
  worker/README.md          # notes for the optional server worker
  tests/sample-inputs/sample.json
  README.md

Options: --id <slug> (override the auto-slug), --dir <path>, --force (overwrite). The default manifest declares files.read, files.write, jobs.create, a Modules/My Module root with imports/exports/workspace folders, browser (required) + python (optional) runtimes, and a 960×620 window.

Inspect the canonical manifest template any time:

powershell
npm --workspace packages/module-cli run hanabi -- module template

3. Develop the UI #

Edit ui/index.html (and any CSS/JS beside it). Use the SDK for platform calls:

ts
import { createHanabiClient } from '@hanabi/module-sdk';
const hanabi = createHanabiClient();

const ctx = await hanabi.ready();                  // signal "loaded" + read context
const { files } = await hanabi.pickFile(['xlsx']); // ask for an input (files.read)
const file = await hanabi.readFile(files[0].id);   // file.bytesB64
await hanabi.writeFile('result.csv', btoa(out));   // write an output (files.write)

createHanabiClient() wraps every capability — pick/read/write files, stream a job (createJob with onProgress/onOutput), durable settings (getSetting/setSetting), notifications + taskbar badges, media streaming (openMedia), and inter-module handoff (listModules / sendToModule). See capability-api.md for the full method list, the envelope, replies, and error codes. Keep these rules in mind:

  • The UI has an opaque originYour UI’s sandboxed identity: it belongs to nobody, so it can’t read the host page, cookies, or other modules. — no cookies, no shell DOM, no direct API.
  • It can only touch files through the bridge, scoped to its own folders.
  • Don't bundle secrets or native executables — the packager rejects them.
Use createHanabiClient() from the SDK, or copy the ~20-line inline client from examples/modules/capability-demo for a plain-HTML module — it speaks the exact same protocol.

Prefer Svelte? Use the bundler

You don't have to hand-write HTML/JS. Author the UI as a Svelte component and compile it to a packaged module with the Svelte → module bundler:

powershell
# src/Module.svelte (uses createHanabiClient) -> ui/  (relative-pathed, iframe-ready)
npm run module:build -- .\path\to\my-module

The component talks to the platform through createHanabiClient() exactly like above — only the authoring language changes. Full working example: examples/modules/svelte-scratchpad (src/Module.svelte → bundled ui/). The bundler reuses the repo's Vite + Svelte (no extra install) and emits relative asset URLs so the host can serve them. To migrate an existing shell panel, copy its markup/logic and swap $lib/api + parent callbacks for createHanabiClient() calls.

Games & rich graphics — Phaser and PixiJS

Because the bundler inlines node_modules imports into the single self-contained ui/index.html, a module can ship a real game or rendering engine. Two are first-class:

  • [Phaser](https://phaser.io) — a full 2D game framework (scenes, Arcade/Matter physics, input, tweens, audio). Reach for it to build a game. Sample: the first-party Hanabi Arcade (bundled_modules/hanabi-arcade).
  • [PixiJS](https://pixijs.com) — a fast 2D renderer (WebGL/WebGPU). Reach for it for visual / canvas / data-art surfaces where you bring your own loop. Sample: examples/modules/demo-pixi.
ts
import Phaser from 'phaser'; // or: import { Application, Graphics } from 'pixi.js';
new Phaser.Game({ type: Phaser.AUTO, parent: el, scene: { create, update } });

What to declare, and what to keep in mind:

  • `client.webgl` (consent) — declare it; the sandbox CSP already allows WebGL, so there's no extra setup. Add `client.fullscreen` for a fullscreen toggle, `client.gamepad` to read gamepads, `client.pointerlock` for mouse-look.
  • No `'unsafe-eval'`. The CSP allows inline scripts but not eval / new Function. Phaser 3 and PixiJS 8 run fine without it — avoid engines or plugins that generate code at runtime. (WebAssembly needs the separate `client.wasm` admin flag.)
  • Everything inlines. Engine code — including libraries that code-split behind `import()` (PixiJS splits its renderer) — is folded into the one file by the bundler, because a sandboxed iframe can't fetch sibling chunks.
  • Persist through the bridge, not localStorage (throwaway in the opaque origin). Hanabi Arcade keeps its high score in `settings.self`.

4. Validate — hanabi module validate #

powershell
npm --workspace packages/module-cli run hanabi -- module validate .\my-module

Checks (identical rules to the backend validator):

  • schema_version is 5.0; id is a valid slug; name/version present.
  • entrypoints.ui is set and stays inside the package; declared entrypoint files actually exist on disk.
  • permissions are all from the allowlist.
  • filesystem.root is under Modules/; every folder has a valid role and a path under the root; folder ids are unique slugs.
  • dependencies shape is valid (known runtimes, office is an object).
  • ui.window sizes are within bounds and self-consistent.

Fix every reported issue before packaging.

5. Test locally #

  • Determinism: keep at least one file under tests/sample-inputs/ — it's required by the packager and is what a reviewer runs your module against.
  • UI smoke test: open ui/index.html in a browser to verify it renders and posts HANABI_READY (check the console / on-page status).
  • Manifest round-trip: module validate after every manifest edit.
  • Worker (if any): keep worker logic deterministic and free of API/database access; it receives a scoped context, not free reign (see `architecture.md` §7).

Test in the Developer Portal debugger (your draft, no approval)

Upload your package, then open the draft's Debug view — it hosts your build in the real sandbox next to a live console. Run worker runs the worker once with no inputs (does it boot?); Run tests runs it against your bundled tests/sample-inputs/. Every capability call, denial, worker line, and error code streams into the console.

*Testing a heavy module before approval. If your module declares `worker.native` (ffmpeg / imagemagick / libvips) or `network.fetch.broad`, those normally need admin approval — but you can smoke-test them first on a capped DEV sandbox: a fixed 1 GB / 90 s tier with the native image and a tiny 10 MB, SSRF-guarded, audited egress budget. It's time-boxed by a 30-minute session (the debugger shows a countdown + a Start new session button); when it ends you're prompted to start a fresh one ([`HANABI-Q009`](error-codes.md#quota-q)). The caps are deliberately small — enough to prove your ffmpeg pipeline or a fetch works, not to run a full job; the production limits come with approval. The capped DEV sandbox only runs on the container* worker backend (the in-process local backend has no native image), and an operator can disable it with HANABI_DEV_SANDBOX_HEAVY=false.

6. Package — hanabi module pack #

powershell
npm --workspace packages/module-cli run hanabi -- module pack .\my-module --out .\my-module.zip

The packager validates first, then builds an upload-ready zip. It:

  • Requires hanabi.module.json + README.md at the root and a non-empty tests/sample-inputs/.
  • Skips .git, node_modules, dist, build, .svelte-kit, .DS_Store, Thumbs.db, and the output zip itself.
  • Blocks secret-like files (.env, secret, credential, password, .pem, .key, id_rsa) and native/script files (.exe, .bat, .cmd, .ps1), and rejects any .. paths.
  • Writes package-root metadata (it won't accidentally nest your folder).

There's a complete browser-only sample to copy from:

powershell
npm --workspace packages/module-cli run hanabi -- module validate .\examples\modules\hello-world-webpage
npm --workspace packages/module-cli run hanabi -- module pack .\examples\modules\hello-world-webpage --out .\examples\modules\hello-world-webpage.zip --force

7. Upload & deploy — Developer Portal #

Before you submit, smoke-test the draft. Uploading sends a brand-new module into the admin review queue, so prove it runs first. Open the draft in the Developer Portal debugger and use Run worker / Run tests (see §5 — Test in the Developer Portal debugger) to watch its bridge calls and worker output with no approval needed. If your module declares a heavy capability (worker.native or network.fetch.broad), exercise that path in the capped DEV sandbox the debugger gives you — a fixed 1 GB / 90 s tier with the native image and a small audited egress budget, inside a 30-minute session — so you confirm the ffmpeg/fetch path works before review rather than after a rejection. Fixing a failure here is far faster than a review round-trip.
  1. Create module details. In the Developer Portal, New module → enter name, summary, category/tags, choose permissions and folders. This calls POST /developer/modules, which assigns a slug id, stores a validated manifest, and creates a draft.
  2. Upload the package. Drop your .zip on the draft. This calls POST /developer/modules/{id}/package, which: - re-runs the full package validation (structure, manifest, safety), - persists and extracts the package so its UI can be served (Phase 1 — live), - records a version entry and moves the draft to pending_review when the package is valid (or needs_changes with issues if not). You can preview your own draft's UI immediately at GET /runtime/modules/{id}/app/ui/index.html — owners may load their draft before it's public.
  3. Admin review (first publish). For a brand-new module, an admin sees the package in the review queue with its automated check results and either approves (→ published) or requests changes (→ back to you with a reason). POST /developer/admin/modules/{id}/review. Later version updates skip this and auto-approve on passing the automated checks — see §9.
  4. Published. The module joins the catalog (GET /modules, GET /store/modules).
Portal UI status: the Developer Portal front-end is wired to the real backend. Create-draft → upload → review → publish is real; analytics come from the C5 usage reports (GET /developer/usage, …/modules/{id}/usage) plus per-module error telemetry (…/modules/{id}/errors); ratings & reviews are live — users rate from the Module Store (GET/POST /store/modules/{id}/reviews), developers reply via POST /developer/modules/{id}/reviews/{review_id}/reply; and support tickets are real threads — opened from the store (/store/tickets) and triaged in the portal (/developer/tickets, owner/admin scoped).

Edit & build in the portal (no local checkout)

For quick fixes you don't need a local checkout. On one of your own draft modules, Edit files opens an in-portal editor over a working copy of the module's built package, seeded from the latest stored build:

  • Browse a file tree and edit ui/index.html, worker/…, hanabi.module.json, and assets; Save writes one file (PUT …/workspace/file), Build version repackages the workspace and runs it through the same validate + security-scan + versioning pipeline as a zip upload (POST …/workspace/build). The built version is reviewed or auto-approved by exactly the §7/§9 rules — the editor is not a shortcut around them.
  • The workspace endpoints are owner-only and apply to draft modules only. Built-in first-party modules are managed in source and can't be edited here. Every write re-applies the package guards (path-traversal, secret files, executables, size), so the editor adds no trust the upload path didn't have.
  • Boundary: the editor changes the built files. To recompile a module's Svelte `src/` (see §3, Prefer Svelte?) build locally with the CLI and upload — or edit the compiled output under ui/ directly.

8. Install & launch #

  • A published module is not installed by default. The user installs it from the Module Store (POST /store/modules/{id}/install), which creates the module's folders and flips it to launchable.
  • Launching opens the module's window. Its assets are served read-only and scoped to installed (or owned) modules at GET /runtime/modules/{id}/app/<entrypoints.ui>.
The generic ModuleHost renders any packaged module's UI in a sandboxed iframe. All eight first-party app modules are now packaged (Phase 4 complete); only genuinely shell-integrated surfaces (File Explorer, Settings, the Store, the Developer Portal) stay as built-in panels — see module-internals.md.

9. Update a module #

Upload a new package version under the same module id. The first publish of a new module needs manual admin review; version updates to an already-published module are auto-approved once the automated checks pass (correct folder structure, required files present, manifest validation, and the safety scans — no secrets/executables, no path traversal, UI entrypoint exists). A version that fails the automated checks is rejected (needs_changes) while the previously published version stays live. Versions are tracked and (for system modules) can be rolled back.

10. Publishing checklist #

  • [ ] module validate passes with zero issues.
  • [ ] tests/sample-inputs/ has a deterministic sample.
  • [ ] Smoke-tested in the debuggerRun worker boots and Run tests passes on your sample inputs, with no unexpected error codes in the console.
  • [ ] Heavy paths exercised — if you declare worker.native or network.fetch.broad, you ran the module in the capped DEV sandbox and the native/fetch path actually works (see §5 and §7).
  • [ ] README.md explains what the module does and what it needs.
  • [ ] Only declared permissions are used (including any desktop.* / files.manage capabilities — declare exactly what you call, nothing more).
  • [ ] No bundled secrets, credentials, or native executables.
  • [ ] dependencies honestly lists runtimes/packages/Office/services.
  • [ ] ui.window sizes make sense for the content.
  • [ ] module pack succeeds and the zip uploads cleanly.

11. Reference #