Platform diagrams
The architecture as pictures: the iframe boundary, file scoping, the lifecycle.
- 1 declared
- 2 granted
- 3 scoped
- 4 REST call
files.read notifications data.storefiles.read.all network.fetchworker.executenetwork.fetch.broadThe same ideas as how-it-works.md — but as diagrams. If you learn better by seeing the shape of a thing than by reading prose, start here. Every diagram has a one-paragraph caption telling you what to take from it.
New to this? Read how-it-works.md first for the narrative, then use these as a map. For the exact messages and methods, see capability-api.md; for the full design, see architecture.md.
Diagrams below use either plain text (ASCII) or Mermaid (the `mermaid blocks render as pictures on GitHub and in most Markdown viewers; if yours shows the code instead, the labels still read top to bottom).
1. The iframe boundary — inside vs. outside #
THE BOUNDARY (a sealed wall) │ ┌──────────────────────────────┐ │ ┌────────────────────────────────────────┐ │ INSIDE the locked box │ │ │ OUTSIDE — the trusted platform │ │ (your module's UI) │ │ │ (the "shell") │ │ │ │ │ │ │ ui/index.html + your JS │ │ │ ┌───────────────┐ │ │ │ │ │ │ The BRIDGE │ the receptionist │ │ sandbox= │ │ │ │ module-bridge │ (checks + acts) │ │ "allow-scripts │ │ │ └──────┬────────┘ │ │ allow-downloads" │ │ │ │ calls, with the user's │ │ (NO allow-same-origin │ │ │ │ real login session │ │ → opaque origin) │ │ │ ┌──────▼────────┐ │ │ │ │ │ │ FastAPI backend│ files · settings │ │ CAN: run its own code, │ │ │ │ + database │ jobs · login │ │ draw the UI, │ │ │ └────────────────┘ │ │ postMessage the parent │ │ │ │ │ │ │ │ Holds: the login cookie, every │ │ CANNOT (on its own): │ │ │ user's files, the database. │ │ read the login cookie │ │ │ │ │ read platform data/DOM │ │ │ │ │ call the server directly │ │ │ │ │ reach other modules/users │ │ │ │ │ │ │ │ │ │ the ONLY way out ─────┼──┼──┼──▶ postMessage ──▶ the bridge │ └──────────────────────────────┘ │ └────────────────────────────────────────┘ │ one wire across, nothing else
Caption. Your module's UI runs inside a locked iframe (left). The platform — login, files, database — lives outside (right). The wall between them is real: the iframe is sandboxed with allow-scripts allow-downloads and, critically, without allow-same-origin, which gives it an opaque origin (an identity that belongs to nobody). That strips it of any power to read the login cookie, see platform data, or call the server. The only thing it can do to the outside world is postMessage the parent window, where the bridgeThe trusted layer that carries every request from your sandboxed UI to the platform, checking permissions. is listening. Everything valuable is on the right; your code is on the left; the bridge is the single, checked doorway between them. (Real sandbox attribute: ModuleHost.svelte.)
2. The message round-trip — one request, matched by rid #
sequenceDiagram participant UI as Your UI (locked iframe) participant BR as Bridge (trusted parent) participant API as Backend (FastAPI + DB) Note over UI: user clicks "Read file" UI->>BR: postMessage { v:1, type:"HANABI_READ_FILE", rid:"r7", fileId:"abc123" } Note over UI: parks a promise under rid "r7", keeps listening Note over BR: 1. is event.source this iframe? Note over BR: 2. DECLARED? manifest lists files.read Note over BR: 3. GRANTED? install granted it Note over BR: 4. SCOPED? abc123 is in THIS module's folders BR->>API: GET file abc123 (with the user's real session) API-->>BR: bytes + metadata BR-->>UI: postMessage { type:"HANABI_READ_FILE_RESULT", rid:"r7", ok:true, data:{…} } Note over UI: listener sees rid "r7" → resolves THAT promise → await returns
Caption. Every capability call is one round trip with the same five beats: envelope out → boundary → bridge checks → backend → reply in. The magic glue is the `rid` (request id): your UI invents it ("r7"), the bridge copies it onto the reply, and your UI uses it to match the answer back to the exact request that asked — which is how several calls can be in flight at once without getting crossed. The reply's type gains a _RESULT suffix and carries ok: true with data, or ok: false with an error. Swap the type and payload and this same diagram describes pickFile, writeFile, notify, every call. (Messages defined in capability-api.md.)
Here's the same trip as a flat ASCII strip, if the sequence diagram didn't render:
YOUR UI BRIDGE BACKEND (locked iframe) (trusted parent) (FastAPI + DB) │ │ │ │ postMessage(req, '*') │ │ │ {type:HANABI_READ_FILE, │ │ │ rid:"r7", fileId:"abc123"} ─▶│ │ │ │ ✓ from my iframe? │ │ │ ✓ DECLARED files.read │ │ │ ✓ GRANTED at install │ │ │ ✓ SCOPED own folder │ │ │ get file abc123 ─────────▶│ │ │◀─────────── bytes + meta │ │◀── {…_RESULT, rid:"r7", │ │ │ ok:true, data:{…}} │ │ │ match rid "r7" → resolve │ │ ▼ ▼ ▼
3. File scoping — your folders vs. the rest of the workspace #
The whole user workspace (everything the signed-in user owns) ┌───────────────────────────────────────────────────────────────────────┐ │ │ │ Modules/ │ │ ├─ YourModule/ ◀── your yard: reads & writes land here │ │ │ ├─ input/ (default for pickFile / readFile) │ │ │ ├─ output/ → Exports (default for writeFile / worker outputs) │ │ │ ├─ workspace/ │ │ │ ├─ cache/ │ │ │ └─ config/ │ │ │ │ │ ├─ SomeOtherModule/ ✗ NEVER reachable — another module's yard │ │ └─ … │ │ │ │ Documents/ Pictures/ Downloads/ … the user's own files │ │ ▲ │ │ └── reachable ONLY with consent-gated files.read.all │ │ (a host "open file" dialog the user drives) — and even then, │ │ still NEVER another module's yard. │ │ │ └───────────────────────────────────────────────────────────────────────┘ default reach : ████ your own folders only with consent : ░░░░ + the user's general files (NOT other modules') never, ever : ✗✗✗✗ another module's folders · another user's anything
Caption. "Scoped" — the third of the three checks — means file operations are clamped to your module's own folders for the signed-in user. By default, pickFile/readFile/writeFile only ever touch your Modules/YourModule/… subtree (writes default to your Exports / output folder). A module that needs the user's broader files must declare and be granted `files.read.all`, which is consent-gated — the user explicitly approves it and picks the file through a host dialog. Even with that grant, the line into another module's folders is never crossed, and neither is another user's data. Name something outside your scope and you get HANABI-F002 ("file outside module scope"). (See capability-api.md and error-codes.md.)
4. The permission tiers — how much trust each capability needs #
flowchart TD A["A module wants a capability"] --> B{Which tier?} B -->|baseline| T1["BASELINE — on by install files.read / files.write · jobs.create settings.self · notifications · storage.read data.store · jobs.queue"] B -->|consent| T2["CONSENT — user approves a prompt files.read.all / write.all client.webgl / gamepad / fullscreen …"] B -->|admin| T3["ADMIN — passes publish review network.fetch · worker.execute client.wasm · jobs.schedule"] B -->|admin-HIGH| T4["ADMIN-HIGH — admin toggle + a quota worker.native · worker.service network.fetch.broad"] T1 --> G["Bridge honors the call only if Declared + Granted + Scoped"] T2 --> G T3 --> G T4 --> G
Caption. Not every capability costs the same trust. The platform sorts them into four tiers, low to high: baseline (the everyday powers, granted just by installing — read/write your files, save settings, notify); consentA one-time prompt the user approves to allow an elevated permission like files.read.all. (off until the user approves a prompt — e.g. reading their broader files, or extra browser features); admin (off until a human reviewer approves your published module — e.g. network access or running a server worker); and admin-HIGH (the heaviest — off unless an admin flips an explicit per-capability switch and sets a resource quota — native media tools, persistent services, broad network). Whatever the tier, the bridge still applies the same final gate at runtime: a call works only if it's Declared + Granted + Scoped. The authoritative list lives in the backend capability registry (services/capabilities.py) and is documented in capability-api.md.
trust required ─────────────────────────────────────────────▶ more BASELINE CONSENT ADMIN ADMIN-HIGH ░░░░░░ ▒▒▒▒▒▒ ▓▓▓▓▓▓ ██████ install grants user approves reviewer approves admin toggle it a prompt the module + a quota files.read/write files.read.all network.fetch worker.native jobs.create client.webgl worker.execute worker.service settings.self client.fullscreen client.wasm network.fetch.broad notifications … jobs.schedule data.store · …
5. The job / worker lifecycle — createJob → sealed sandbox → outputs #
flowchart LR UI["Your UI createJob(options, inputRefs)"] -->|HANABI_CREATE_JOB| BR[Bridge] BR -->|gather input files as bytes| BOX subgraph BOX["Sealed worker sandbox (a throwaway container)"] direction TB IN["inputs: { filename: bytes }"] --> RUN["your worker: run(inputs, options)"] RUN --> OUT["returns output files"] NONET["NO network · NO database NO session · NO other files"] end OUT -->|collected| EXP["Your module's Exports folder"] RUN -. "yield {pct, message}" .-> PROG["live progress → HANABI_JOB_PROGRESS → your UI"] EXP --> USER["User opens / downloads the result"]
Caption. When a module needs heavy or native work (image conversion, spreadsheet building, video transcode), it ships a small Python worker and triggers it with createJob(...). The platform gathers the job's input files as plain bytes, drops them into a throwaway, sealed container — no network, no database, no session, no other files — and runs your one function, run(inputs, options), which returns output files. The platform collects those into your Exports folder for the user. A long job can `yield` progress, which streams back to your UI as live HANABI_JOB_PROGRESS events (one small JSON line at a time) so you can show a progress bar. Because the worker is strictly "bytes in → bytes out," the platform can run code it didn't write; that's also why running a third-party worker is an admin-approved capability and the sandbox ships off by default. (Contract: worker-guide.md; isolation: worker-sandbox-design.md.)
YOUR UI BRIDGE SEALED SANDBOX OUTPUT createJob(...) ──▶ gather inputs ──▶ ┌──────────────────┐ │ inputs (bytes) │ │ run(inputs, │ Exports/ │ options) │ ──▶ (your files) │ → outputs │ │ │ no net · no DB │ ▼ │ no session │ user opens / └──────────────────┘ downloads them │ yield {pct,msg} ▼ live progress ──▶ HANABI_JOB_PROGRESS ──▶ your UI
Putting it together — the whole lifecycle, one strip #
author ─▶ pack(zip) ─▶ upload ─▶ validate ─▶ admin review ─▶ install ─▶ launch ─▶ bridge ─▶ (worker) ─▶ output you hanabi CLI Portal automatic (if elevated) per-user iframe caps sealed Exports
Caption. Zoomed all the way out, a module's life is a straight line: you author it, pack it into a zip with the hanabi CLI, upload it in the Developer Portal, where it's automatically validated and (if it asks for elevated powers) sent to admin review; a user installs it, launches it (the locked iframe of diagram 1), it talks to the platform through the bridgeThe trusted layer that carries every request from your sandboxed UI to the platform, checking permissions. (diagrams 2–4), optionally runs a workerAn optional Python program that does heavy or native work off the browser, in a sealed container. (diagram 5), and produces output. Each diagram above zooms into one segment of this line. Full lifecycle table: `architecture.md` §4.
Where to read next #
how-it-works.md— the narrative version of every diagram here.capability-api.md— everyHANABI_*message and SDK method, and the permission tiers.architecture.md— the full platform design and roadmap.worker-guide.md— the worker contract behind diagram 5.error-codes.md— every error code, what it means, and how to fix it.examples.md— sample modules to read and run, easiest first.