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
Capability bridge
  1. 1 declared
  2. 2 granted
  3. 3 scoped
  4. 4 REST call
Platform
REST API
Every call is declared, granted and scoped before it touches the platform.
Baseline
granted on install
files.read notifications data.store
Consent
user prompt
files.read.all network.fetch
Admin
approved at review
worker.execute
Admin · HIGH
review + quota
network.fetch.broad
Higher tiers need more approval.
Inputs
files options
Worker
sealed container
no network by default
Outputs
files
A worker is bytes in, files out.
Scaffold
Validate
Package
Upload
Review
Publish
Install
From your machine to every user's window.
My Module
Drop a file
Your UI runs in a sandboxed window.

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