Hanabi Developer Hub
/ Help / Beginner FAQ
Help

Beginner FAQ

The "wait, why doesn't this work?" gotchas every new module developer hits.

Every one of these has tripped up someone before you. None of them mean you did anything wrong — they're the consequences of how the platform keeps modules safe, and once you know the why, the fix is usually one line. Each answer gives you the reason, the fix, and the error code to look up when one applies.

New to the words in here (sandbox, scope, worker, manifest)? Keep glossary.md open in another tab.


Why does my localStorage not save anything? #

What you see: you call localStorage.setItem(...), reload the window, and it's empty. No error, just… gone.

Why: your module's UI runs in a sandboxed iframe with an opaque originYour UI’s sandboxed identity: it belongs to nobody, so it can’t read the host page, cookies, or other modules. — a locked box with no identity (see glossary.md). Browsers tie localStorage, cookies, and IndexedDB to a page's identity, and an opaque origin has none, so that storage is throwaway — it's wiped the moment the window closes. The platform does this on purpose: it means one module can never read another module's saved data.

The fix — use the platform's storage instead, which is durable and per-user:

  • A few values (a setting, a preference, the last thing typed)? Use `settings.self`. It saves server-side per (user, module), so it survives a reload, a cache clear, and follows the user to another device.
js
  await hanabi.setSetting('theme', 'dark');     // saved for good
  const theme = await hanabi.getSetting('theme');

Working sample: examples/modules/demo-settings.

  • Lots of structured records (a to-do list, an inventory, a library)? Use the data storeBaseline storage of JSON documents in named collections (data.store), for app-like state. (data.store) — collections of JSON documents, also scoped to (user, module). Sample: examples/modules/inventory-tracker.

Both are baseline permissions: declare them in your manifest and they work immediately, no review needed. See the full methods in capability-api.md.


Why can't my module fetch() a website? #

What you see: fetch('https://example.com/...') from your UI throws, or the browser console complains the request was blocked by a content-security policy.

Why: two layers are stopping you, both by design.

  1. The sandbox ships a strict CSP (see glossary.md) that blocks your page from contacting any web address it didn't declare in advance — so a module can't quietly phone home or leak your files.
  2. Even with the address declared, reaching out to the network is a reviewed capability, not a free one.

The fix — pick the right path for what you're doing:

  • The UI needs to call a specific API → declare that exact origin in your manifest under dependencies.services, and request the `network.fetch` permission (an admin-reviewed capability). Only the origins you list become reachable.
json
  "dependencies": { "services": ["https://api.github.com"] },
  "permissions": ["network.fetch"]
  • The work is heavy, private, or processes the response → do it in a workerAn optional Python program that does heavy or native work off the browser, in a sealed container. instead of the UI. The worker fetches through the platform's guarded proxy. Same dependencies.services + network.fetch. See examples/modules/demo-fetch-worker and worker-guide.md.

Related error codes:

  • HANABI-N007Network not approved: you tried to use the network with no network.fetch permission. Request it.
  • HANABI-N001Network origin not approved: you have network.fetch, but the address you hit isn't in your declared dependencies.services. Add it (or, for open access, request network.fetch.broad, an admin-HIGH capability).
Heads-up: workers can only reach public addresses — a fetch to a private or loopback host is blocked (HANABI-N002).

Why can't I open a file on the Desktop? #

What you see: you try to read some file by its path or id and get File outside module scope.

Why: every module has a scopeThe rule that file operations stay inside your own Modules/<Name>/ folders for the current user. (see glossary.md) — by default it can only touch files inside its own folders, under Modules/<Your Module>/. The Desktop, other modules' folders, and other users' files are all off-limits. This is the rule that stops a module you installed from rummaging through everything else you own.

The fix — choose the narrowest thing that works:

  • Read/write your own files → keep your inputs and outputs in your declared folders and use pickFile(), readFile(), and writeFile(). This is baseline — nothing to request. Sample: examples/modules/demo-files.
  • The user picks a file from anywhere → request `files.read.all` (a consentA one-time prompt the user approves to allow an elevated permission like files.read.all. capability — the user is asked once), then use pickUserFile(), which opens a real file-open dialog over the user's workspace. (Still never other modules' folders.)
  • You were launched via "Open with" → the single file the user chose is readable for that launch with plain files.read, even though it lives outside your folders — because the user explicitly handed it to you. See capability-api.md.

Related error code: HANABI-F002File outside module scope. (If instead you get HANABI-F003No writable folder — you forgot to declare an output or workspace folder in your manifest's filesystem.folders.)


Why did my worker get killed? #

What you see: your Python worker starts, then dies partway through — sometimes with a timeout, sometimes just gone.

Why: workers run inside a hardened container with resource limits for their tier — a cap on how long they can run and how much memory they can use. This keeps one runaway module from hogging the whole server. Cross a limit and the platform stops the worker rather than letting it run away.

The fix:

  • It ran too longHANABI-W004Worker timed out. Do less per job, make the code faster, or ask an admin for a larger resource tier (part of your worker.execute quota).
  • It used too much memoryHANABI-W005Worker out of memory. The usual cause is loading a huge file all at once; stream it instead with the run_streaming contract (process the file in pieces by path), or request a bigger tier. See examples/modules/demo-streaming-worker.
  • It threw an exceptionHANABI-W003Worker crashed. That's an ordinary bug in your code, not a limit. Open the debugger, read the traceback, and handle the failing input. (See How do I see what went wrong? below.)

A worker is meant to be a small, fast inputs → outputs step. If a job feels too big for one run, that's the signal to stream it or split it. More in worker-guide.md and limitations.md.


Why was my package rejected at upload? #

What you see: you build the .zip, upload it, and it bounces with a validation error before it ever installs.

Why: before a package is accepted, the platform validates it — the manifest must be well-formed, required files must be present, and nothing dangerous can be inside. It's a spell-check for your module, and it's strict so a broken or unsafe package never reaches anyone.

The common rejections and their fixes:

  • HANABI-V007Missing package file: a required file isn't there (the hanabi.module.json at the root, the README.md, or the tests/ sample inputs). Add it — running module pack tells you exactly what's required.
  • HANABI-V008Secret file in package: something that looks like a credential (a .env, a key, a token) got swept in. Remove it; secrets never belong in a module.
  • HANABI-V009Blocked file type: the zip contains a blocked runtime file, like an executable. Ship source, not binaries — remove it.

Almost always the cure is to let the CLI build the package instead of zipping by hand — it lays out the right files and strips the wrong ones for you:

bash
npm --workspace packages/module-cli run hanabi -- module pack examples/modules/inventory-tracker --out inventory-tracker.zip

Other validation codes (a malformed manifest is HANABI-V001; a bad module id is HANABI-V003) follow the same idea — the message names the field. See packaging.md and the full V list in error-codes.md.


How do I see what went wrong? #

The short version: you're not meant to guess. The platform gives you a code and a console for nearly everything.

  1. Read the error code. Every failure carries a stable code like HANABI-F002. The developer debugger shows it; look it up in error-codes.md for the meaning and the fix. The code never changes, so you can also search the docs for it.
  2. Use the developer debugger. In the Developer Portal, the debug console shows your module's bridge requests and the replies (including any error code), so you can watch exactly which call failed and why.
  3. Run the worker on its own. If a worker is misbehaving, run it from the portal to see its output and full Python traceback — that's where HANABI-W003 (Worker crashed) shows you the actual exception. Run worker runs it once with no inputs (does it boot?); Run tests runs it against your module's own tests/sample-inputs/ files so you can watch how it handles real data.
  4. Heavy module? Test it before approval. If your module needs native tools (worker.native) or open-web fetch (network.fetch.broad), you don't have to wait for an admin — the portal runs it on a capped DEV sandbox (a small 1 GB / 90 s tier with the native image + a tiny audited egress budget) inside a 30-minute session. It's enough to prove your ffmpeg/fetch path works; the full limits come with approval.

When you report a problem, quote the error code — it points straight at the cause. See the end-to-end build/test loop in developer-guide.md.


Do I need to know Python? #

Short answer: only if you add a worker.

A huge number of useful modules are just a webpage — a single ui/index.html written in HTML and a little JavaScript — plus the bridge. Calculators, note-takers, to-do lists, editors that use the data store: none of those need a worker, so none of them need a line of Python. Start at examples/modules/hello-world-webpage.

You only reach for Python when you add the optional workerAn optional Python program that does heavy or native work off the browser, in a sealed container. (see glossary.md) — the server-side half that does heavy or private computing the browser can't (or shouldn't) do: converting media, crunching big files, calling an API privately. That's the only place Python appears. If you never add a worker, you never touch it. When you're ready, the worker contract is gentle and well-documented in worker-guide.md.


Still stuck? #

None of these mistakes are a big deal — they're the platform doing its job. Keep going.