花 Hanabi Developer Hub
/ Advanced / Platform diagrams
Advanced

Platform diagrams

The architecture as pictures: the iframe boundary, file scoping, the lifecycle.

Module UI
sandboxed iframe · opaque origin
postMessage
Capability bridge
trusted parent · checks permission + scope
auth API
Platform
files · jobs · data · settings
reply { rid, ok, data } — or { ok: false, error: { code } }
Every call: declared → granted → scoped → audited
Inside the locked box
your module's UI · opaque origin
Run its own code & draw the UI
postMessage the parent
Can't read the login cookie
Can't call the server directly
Can't reach other modules
Outside — the platform
the trusted shell
The bridge — checks & acts
Holds the login session
Every user file & the database
FastAPI backend
One wire across the wall — postMessage, nothing else
Modules/YourModule/
your yard — reads & writes land here
default
Documents/ · Downloads/
the user's wider files — only with files.read.all
consent
Modules/OtherModule/
another module's folders — no permission reaches here
never
Another user's files
never visible to your module
never
Out-of-scope requests fail with HANABI-F002
baseline
install
files.readdata.storenotifications
consent
user prompt
files.read.allclient.webgl
admin
publish review
network.fetchworker.execute
admin-HIGH
review + quota
worker.nativeworker.service
Higher tiers = more power = more gating
inputs
{ name: bytes }
the files you picked
run()
run(inputs, options)
isolated sandbox · resource-capped
return
outputs
{ name: bytes }
written to Exports
A worker is a pure transform — no files, no network, no other users
Author
you
›
Pack
CLI
›
Upload
Portal
›
Validate
auto
›
Review
if elevated
›
Install
per user
›
Launch
iframe
›
Bridge
caps
›
Worker
sealed
›
Output
Exports
A module's whole life, end to end

The 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 #

text
                         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 #

mermaid
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:

text
  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 #

text
   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 #

mermaid
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.

text
   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 #

mermaid
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.)

text
   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 #

text
  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 #