Skip to content

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.

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

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

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

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

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

PathUse
/welcomeFirst-run create or import
/unlockPassword unlock
/dashboardBalances and primary actions
/accountsAccount list, pin, hide, order
/accountSingle-account detail
/add-accountDerive or import another account
/keyringKeyring management
/exportPassword-gated secret export
/sendNative or token send, then review
/receiveAddress and QR
/faucetBetanet faucet claim
/historyDecoded activity stream
/settingsNetwork, lock, window, security
/resetDestroy 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.

  1. v5Existing signing methods moved to auth: 'signing'.
  2. v6Reset confirmation and auto-lock changes hardened in the background.
  3. v7Custom-network activation quarantined. Stale selections self-heal to the default network.
  4. v8token.transfer and a real token.getBalances on official program bindings.
  5. v9History feed.
  6. v10History detail.
  7. v11Checked sends. Reviewed account and network bind at the background.
  8. v12Unlocked-only tx.registerAccount for an owned address, plus storage-only history cache.
  9. v13tx.claimFaucet drops to unlocked auth and sheds its vestigial password param. Shipped in 1.4.1.
  10. v14token.readMint verifies a pasted contract address on-chain before it becomes a custom token. Shipped in 1.4.1.
  11. v15tx.checkDuplicate plus an optional allowDuplicate on the send methods. Shipped in 1.4.1.
  12. 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.