Architecture
Authority sits in the background.
5 layers. 14 routes. Contract v15 with 83 methods in the published build, v12 in a status document that has not caught up. The UI asks. The background decides.
01
UI route stack
src/ui/app/routes/*
Fourteen real routes share one guarded DOM kit, router, modal, focus trap, and popup/side-panel page. There is no legacy popup fallback.
02
Bridge seam
bridge.send(method, params)
The UI has one outbound Chrome message caller. BigInt values become strings before they cross the port. Nothing unserializable is allowed through.
03
API router
src/background/api-router.js
Every request is checked against the append-only manifest, auth tier, sender context, and JSON-serializable contract. Unknown methods fail closed.
04
Services
src/background/services/*
Account, network, transaction, token, history, settings, registration, and vault orchestration live in the background. The UI does not import them.
05
Sacred adapters
src/lib/vault.js · thru-client.js · networks.js
Crypto, session, and keyring; Thru RPC, transaction, and program code; and network config stay behind strict import boundaries. Program addresses are read from the pinned @thru/programs release, not pasted in as strings.
Boundaries
These are enforced by tests where the repository can enforce them, and by review where it cannot.
- UI never imports background services, vault internals, or RPC internals.
- Background owns auth and signing.
- src/shared/contract/manifest.js is the API allowlist.
- src/ui/kit/dom.js is the guarded DOM factory. The sink ratchet is zero.
- Money is BigInt internally and a string on the wire. Never both in one object.
- Network-specific data belongs in network config, not a module constant. Adding a network is one entry plus a CSP origin, and the check script fails if those two disagree.
- Thru is not EVM. Other wallets are UX references only.
Fourteen routes
One stack. No legacy popup fallback. Every route is mounted by the lifecycle test in no-vault, locked, and unlocked states.
| Path | Use |
|---|---|
| /welcome | First-run create or import |
| /unlock | Password unlock |
| /dashboard | Balances and primary actions |
| /accounts | Account list, pin, hide, order |
| /account | Single-account detail |
| /add-account | Derive or import another account |
| /keyring | Keyring management |
| /export | Password-gated secret export |
| /send | Native or token send, then review |
| /receive | Address and QR |
| /faucet | Betanet faucet claim |
| /history | Decoded activity stream |
| /settings | Network, lock, window, security |
| /reset | Destroy the local vault |
Documented contract breaks
The contract is append-only, except these security changes, which are called out rather than hidden. Steps v13 to v15 shipped in the 1.4.1 package, published to the store on 2026-10-04.
- v5Existing signing methods moved to auth: 'signing'.
- v6Reset confirmation and auto-lock changes hardened in the background.
- v7Custom-network activation quarantined. Stale selections self-heal to the default network.
- v8token.transfer and a real token.getBalances on official program bindings.
- v9History feed.
- v10History detail.
- v11Checked sends. Reviewed account and network bind at the background.
- v12Unlocked-only tx.registerAccount for an owned address, plus storage-only history cache.
- v13tx.claimFaucet drops to unlocked auth and sheds its vestigial password param. Shipped in 1.4.1.
- v14token.readMint verifies a pasted contract address on-chain before it becomes a custom token. Shipped in 1.4.1.
- v15tx.checkDuplicate plus an optional allowDuplicate on the send methods. Shipped in 1.4.1.
- v16Removes tx.send and token.transfer, the unbound mutation paths left after every caller moved to the checked methods. 83 methods become 81 — the first time the count has gone down. Written on the open #17 to #18 chain, not merged, not in any package.