The web-sdk does not know bootstrap exists. A fresh TinyCloud account needs 5 enshrined spaces, 2 SQL schemas, registry seeding, and an encryption network (~10 root signatures + a dozen node operations). All of it happens server-side: when the user signs their single sign-in SIWE through the OpenKey widget, the TEE — which holds the sealed root key — performs the entire bootstrap against the tinycloud-node directly. No round-trips return to the frontend beyond the one signature.
Failure policy: if the server-side bootstrap fails, nothing breaks — the user simply creates host delegations the default way (interactive prompts, lazily, as features need them). Accepted worst case; no prompt flood, no bricked sign-in, no client-side compensation.
Client-orchestrated bootstrap (the browser driving per-step signatures through /api/delegate/sign) was briefly deployed on 2026-07-02 and has been removed from the browser path (listen#91). It survives only for node-sdk/CLI contexts that hold local keys and have no TEE.
flowchart LR
subgraph BROWSER["🖥 Browser (listen.tinycloud.xyz)"]
APP["Listen app"]
WSDK["@tinycloud/web-sdk
(bootstrap-unaware)"]
WIDGET["OpenKey widget iframe"]
APP --> WSDK
WSDK -- "ONE sign-in SIWE" --> WIDGET
end
subgraph TEE["🔒 OpenKey API — Phala dstack TEE (api.openkey.so)"]
KSIGN["POST /api/keys/:id/sign"]
HOOK["ensureTinyCloudBootstrapForApprovedSign
(the bootstrap orchestrator)"]
SEAL["TEE-sealed root key"]
STATE["tinyCloudBootstrapState
(idempotency + locking)"]
KSIGN --> SEAL
KSIGN --> HOOK
HOOK --> SEAL
HOOK --> STATE
end
subgraph NODE["☁️ tinycloud-node"]
VERIFY["verify + provision:
5 spaces · schemas · registry ·
encryption network"]
end
WIDGET -- "sign message" --> KSIGN
HOOK -- "sessions · host delegations ·
activation · SQL · seeding" --> VERIFY
KSIGN -- "signature + tinycloudBootstrap status" --> WIDGET
style TEE fill:#0e211b,stroke:#1f4d3d
style BROWSER fill:#0d1d29,stroke:#1e4055
style NODE fill:#161b28,stroke:#232a3a
sequenceDiagram autonumber actor U as User participant L as Listen (browser) participant W as OpenKey Widget participant K as TEE: /api/keys/:id/sign participant B as TEE: bootstrap orchestrator participant N as tinycloud-node U->>L: sign in L->>W: sign-in SIWE (the ONLY prompt) U->>W: ✍️ one signature W->>K: sign message K->>B: TinyCloud SIWE detected → bootstrap if needed Note over B: autoSignEnabled? fresh account?
lock tinyCloudBootstrapState B->>B: create 5 bootstrap sessions +
5 host SIWEs (sealed key, zero gestures) B->>N: submit host delegations ×5 B->>N: activate sessions ×5 B->>N: account-index schema (execute + schema stmts) B->>N: seed spaces/<fullSpaceId> KV + SQL index rows B->>N: seed application manifests B->>N: secret_records schema · encryption network B-->>K: outcome (complete | skipped | failed) K-->>W: signature + tinycloudBootstrap status W-->>L: signed in Note over L,N: browser ↔ node traffic from here on is normal app usage —
the SDK sees a fully provisioned account (index rows exist) alt bootstrap failed (worst case) Note over L: nothing breaks — host delegations get
created the default interactive way, lazily end
stateDiagram-v2 [*] --> Received : widget signs a TinyCloud SIWE Received --> Skipped : kill-switch off /
raw sig / not TinyCloud /
autoSign disabled Received --> CacheHit : bootstrapState complete Received --> Locked : concurrent attempt
holds the lock Received --> Executing : fresh account Executing --> Complete : sessions → host →
activate → schema →
seed → encryption Executing --> Failed : any step throws Complete --> Recorded : state row = complete Failed --> Surfaced : logged + tinycloudBootstrap
field in /sign response Recorded --> [*] CacheHit --> [*] Skipped --> [*] Locked --> [*] Surfaced --> [*] note right of Surfaced worst case: user creates host delegations the default way. Not a problem — by design. end note
TINYCLOUD_BOOTSTRAP_ON_SIGN=off.stateDiagram-v2 [*] --> SigningIn SigningIn --> GateCheck : session established GateCheck --> Skipped : interactive signer
(ALL browser flows now) GateCheck --> ClientBootstrap : non-interactive signer
(node-sdk/CLI with local key) ClientBootstrap --> Done : provisioned client-side ClientBootstrap --> Degraded : failure → bootstrapSkipped
signIn() still resolves Skipped --> SignedIn Done --> SignedIn Degraded --> SignedIn SignedIn --> [*] note right of Skipped browsers ALWAYS land here: listen#91 removed the strategy, so the SDK never orchestrates bootstrap in a browser end note
Status: resolved. Both bugs found in prod TEE logs, fixed in openkey#122, deployed to Phala 2026-07-03. Not hypotheses — confirmed from live logs and code.
The orchestrator sent the SQL schema with a SELECT 1 carrier under action:'execute'. The node's SQLite rejects row-returning statements under execute:
Every single server-side bootstrap failed here — after hosting spaces but before creating the account index. This is the failure that was silently swallowed for weeks (until the surfacing fix made it visible in logs) and the mechanism behind js-sdk issue #300. Fix: the carrier is now the last idempotent schema statement.
The orchestrator seeded spaces/<shortName> KV records with type:'private', KV-only. The SDK expects spaces/<fullSpaceId> with type:'owned' + permissions:['*'] and mirrored account-index SQL rows. Result: isFreshBootstrapAccount() stayed true forever — every sign-in looked like a first sign-in. Fix: the orchestrator now writes byte-compatible records, KV + SQL, schema-first.
The interim client-orchestrated path (deployed earlier on 07-02) ran concurrently with the server hook on the same fresh accounts: the browser drove ~10 /api/delegate/sign calls + node writes while the TEE hook independently hosted the same spaces and died at RC1. Combined with RC2 (accounts never look provisioned), every sign-in re-triggered both engines. Fix: listen#91 removed the client engine from the browser entirely.
Before any of this, web-sdk 2.4.0 had no non-interactive path at all, so the client bootstrap pushed all 10 signatures through the widget — the original TC-86 report. The interactive-skip gate (js-sdk 2.5.0) now guarantees browsers never do this, permanently.
| When | What | Status |
|---|---|---|
| 06-28 | Autosign spec + PRs #296/#108/#117: manifest, policy gate, /api/delegate/sign, server hook | kept |
| 07-02 | js-sdk 2.5.0: interactive-skip gate, purpose tags, degrade-to-skipped; web-sdk signStrategy forwarding | kept (gate) / unused in browser (strategy plumbing) |
| 07-02 | Listen wires client-orchestrated bootstrap via /api/delegate/sign (listen#80/#90) | reverted by #91 |
| 07-03 | Architecture locked: bootstrap is TEE-only. Server orchestrator fixed (openkey#122); browser strategy removed (listen#91). Worst-case failure = default interactive hosting. | current |
Remaining follow-ups: live fresh-account verification (watching prod logs for the first post-deploy bootstrap), /api/delegate/sign rate limit, cosmetic kv// path rendering in bootstrap recaps.