Hanabi Developer Hub
/ Build / Convert an existing app
Build

Convert an existing app

Turn an HTML/JS app or a Python script into a sandboxed module — the mechanical diff.

Inputs
files options
Worker
sealed container
no network by default
Outputs
files
A worker is bytes in, files out.

Most existing apps become modules with a small, mechanical diff. There are two paths:

  • a UI app (HTML/CSS/JS) → a browser module (its UI runs in the sandboxed iframe),
  • a script / CLI (e.g. Python) → a worker module (its logic runs in the sandbox).

Worked, runnable before/after pairs live in examples/converting/.


Part 1 — an HTML/JS app → a browser module #

Worked example: examples/converting/html-todo (before/index.html is a plain localStorage to-do; after/ is the module).

A module's UI runs in a sandboxed, opaque-origin iframe, so a few browser APIs are unavailable and are replaced by capability calls. Three changes:

1) Add a manifest

Add hanabi.module.json at the root and put your page at ui/index.html. Declare the permissions for the capabilities you'll use (see step 2). See manifest-reference.md.

2) Swap browser-only APIs for capabilities

Your app does……becomes (bridge message / SDK)Why
localStorage / sessionStoragesettings.self (HANABI_GET/SET_SETTING) for a value, or data.store (HANABI_DATA_*) for recordsthe opaque-origin iframe has no persistent storage; these are durable + cross-device
<a download> / save a BlobHANABI_WRITE_FILE → the module's Exports (downloads also still work)files land in the user's workspace, not just the browser
<input type=file> to open a fileHANABI_PICK_FILE + HANABI_READ_FILEscoped to the module's folders + the file the user opened it with
fetch() to an APIdeclare the origin in dependencies.services + network.fetch (UI), or do it in a workerAn optional Python program that does heavy or native work off the browser, in a sealed container.the served CSP blocks undeclared connect-src
<video src="file…"> for a big local fileHANABI_OPEN_MEDIA (a range URL)streams + seeks without copying bytes
cookies / its own auth(remove) — the platform already authenticates the usera module never sees credentials

3) Add the bridge client

Paste the ~12-line call() client (top of any demo-* ui/index.html) and run your init code after HANABI_READY. The after/ to-do shows the entire diff — the DOM/render code is untouched; only persistence (localStoragesettings.self) and export (downloadwriteFile) change.

Prefer types? createHanabiClient() from @hanabi/module-sdk wraps the same messages.

Part 2 — a Python script/CLI → a worker module #

Worked example: examples/converting/python-wordcount (before/wordcount.py is a plain CLI; after/ is the module).

A worker is `run(inputs, options, ctx=None)` — keep your logic, replace the I/O shell:

Your script does……becomes
read sys.argv / input()read `options` (the dict the UI passed to createJob)
open(path) / read filesiterate `inputs`{filename: bytes} the platform supplies
print(...) / write output filesreturn {filename: bytes|str} — each becomes a file in Exports
requests.get(url) / socketsdeclare the third `ctx` arg + network.fetch; call ctx.fetch(url) (the container has no network)
pip install foodeclare it in dependencies.python: ["foo>=1.0"]
if __name__ == "__main__":(delete) — the platform calls run
subprocess to ffmpeg/imagemagickdeclare worker.native (admin-HIGH) to get them on PATH

The after/worker.py keeps count_words() byte-for-byte; only the wrapper changes. Declare entrypoints.worker + worker.execute in the manifest. The worker runs isolated (no db, no network, no other user) — see worker-guide.md.

What you DON'T get in a worker

No host filesystem, no environment/secrets, no persistent process (one-shot bytes→bytes), no network except host-mediated ctx.fetch. A worker that needs to stay resident is a persistent service (worker.service, admin-HIGH) — a separate, admin-managed path.


Then: package + test #

bash
npm --workspace packages/module-cli run hanabi -- module validate ./my-module
npm --workspace packages/module-cli run hanabi -- module pack ./my-module --out ./my-module.zip

Upload it in Module Store → Upload. See packaging.md for details and limitations.md for the full list of sandbox constraints to design around.