Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Trustless webmail

The webmail app normally rides its account API for everything: login, folders, compose, settings. Trustless mode is the same app with the mail server removed from the loop entirely — leave the API URL blank in the connection settings pane and, from the next load, the wallet alone unlocks the session (no login challenge, no JWT) and every on-chain read and write goes straight to a Solana JSON-RPC node. PDA derivation, account decoding, sealing, and signing all happen in the page, in the same WebAssembly module the normal mode uses; no server ever sees your key, and now no server has to exist at all.

Note the default is not blank: the app is normally served same-origin by account-api, and an absent setting means “this origin”. Trustless mode is the explicitly cleared field — the deliberate statement that there is no account API.

What it needs instead

Three endpoints, all in the connection settings pane (dev defaults in parentheses — the local development stack):

  • Solana RPC URL (http://127.0.0.1:8899, a surfpool dev validator) — every chain read and the transaction submit path.
  • IPFS gateway URL (http://127.0.0.1:8183, sithbit-gateway) — GET /ipfs/{cid} for reading sealed bodies.
  • IPFS pin service URL (http://127.0.0.1:8182, sithbit-ipfsd, plus its optional bearer token) — where sending pins outbound bodies. Only sending needs it; reading works with the first two alone.

One sithbit-ipfsd can serve both IPFS roles: its pin surface is POST /pins/… and it answers GET /ipfs/{cid} for anything it holds, so pointing the gateway and pin URLs at the same daemon is a complete single-node setup.

What works, and what doesn’t

Everything on-chain works: wallet unlock, the trustless inbox (your mailbox’s message count enumerated newest-first, straight off the message accounts), the trustless reader (fetch the sealed body from the gateway, unseal with the wallet in the page, render), balances, delegated encryption-key management, and the trustless compose described below.

Everything that lives on a server doesn’t, and the app hides those surfaces rather than degrade them: the server mail view (folders, search, the Sent copy), compose through the server (and with it any delivery to non-SithBit addresses — trustless send is on-chain only, there is no plaintext fallback), account settings (mail password, timezone, do-not-disturb), and the marketplace and alias listings — those ride the gateway’s off-chain index, which a raw RPC node cannot answer. Resolving an alias you send to still works: that is a plain account read.

Sending without a server

The trustless compose card follows the same path sithbit mail send takes, client-side:

  1. Resolve the recipient — a wallet address or alias — and their published encryption key from the chain.
  2. Check their mailbox. A mailbox with the no_ipfs opt-out refuses the send outright: the flag means “my mail never touches public IPFS”, a trustless client cannot deliver any other way, and the opt-out is honored client-side before anything is pinned.
  3. Check your frombox toward them. No frombox yet? The card quotes the postage and points at the Balances pane to buy stamps — your draft stays intact.
  4. Seal the message to the recipient’s key in the page, pin the sealed bytes to your configured pin service, then sign and submit the SendMail transaction and poll it to commitment.

The signing step follows your session’s wallet flavor. An unlocked in-page wallet signs the transaction right in the page. A connected external wallet (Phantom or Ledger) is asked to approve the same transaction instead — the page builds it unsigned with your wallet as the fee payer, your wallet extension signs and broadcasts it, and the page polls the returned signature to commitment. Either way the sealing happens in the page before anything leaves it; the external wallet only ever sees the finished transaction.

Replying, and attaching a bounty

The trustless reader’s Reply on-chain action seeds this compose card: the To field takes the decrypted sender (for a local-only message, the sealed envelope’s sender), the subject gets its Re: prefix, and the draft carries the parent message’s account address — the same privacy-preserving linkage --reply-to makes, so only its blake3 hash reaches the chain. A chip on the card shows the linkage; Clear drops it and keeps the draft. Replying does not require a bounty.

Nor does replying require trustless mode: even with an account API configured, opening a message in the trustless viewer and clicking Reply on-chain opens this card, prefilled the same way — it floats in the corner exactly like the server compose pane. (Earlier releases seeded the draft but left the card hidden in API mode, so the click appeared to do nothing.)

The card’s Attach a reply bounty fields escrow SOL on the message exactly like --bounty/--bounty-window: an amount in SOL (blank = plain send) and a claim window in days (default 7, the CLI’s default window). A window that would make the bounty born expired is refused in the page before anything is pinned — the same rule the chain enforces. Both signing flavors author bounties; what doesn’t is compose through a mail server, which stays bounty-less by design — bounty authoring is a direct-signed surface.

No frombox yet? Prepay inline

When step 3 finds no frombox toward the recipient, the card holds your draft and quotes the purchase instead of failing: a stamp-count input (default 1 — the on-chain prepayment rule: a non-owner’s first purchase is at least one stamp) and the exact funding math per stamp — the recipient’s posted postage, the fixed settlement surcharge, and the postoffice’s live per-stamp protocol fee, read off the chain rather than assumed from its genesis default (it is waived only when you buy toward your own mailbox), plus the account rent the chain adds on a first purchase.

Prepay & send buys the stamps with whichever wallet flavor your session runs — the in-page wallet signs directly; an external Phantom/Ledger wallet approves the same purchase built unsigned — and, once the purchase confirms, automatically re-sends the held draft, bounty and reply linkage included. Whether the purchase opens the frombox or tops it up is decided by a fresh chain read at click time, exactly like frombox stamp’s create-on-first-purchase. A purchase still pending past the poll budget does not auto-retry (it would only bounce off the stamps check) — the card keeps the draft and says to send again shortly. The Balances pane remains the standing place to buy stamps outside a compose, and its purchases ride both signing flavors too, exactly like this card’s own Prepay & send: the in-page wallet signs directly, an external Phantom/Ledger wallet approves the same purchase built unsigned.

The receipt line carries the on-chain message id and transaction signature. There is no Sent folder in this mode — the on-chain message account is the record.

The pin lifecycle caveat (operators, read this)

A body pinned by a trustless client sits outside any mail server’s pin lifecycle. When mail is deleted or settled, a sithbitd operator’s pipeline releases the pin its own delivery made — it has no idea your daemon holds a client-made pin for the same message, and it will never unpin it. The client’s pin is the client’s lifecycle, exactly like the per-recipient pin providers’ standing rule that settlement releases the operator’s pin, never yours: a client-pinned body stays on the daemon it was pinned to until someone removes it there.

So an operator offering a pin service to trustless webmail users is signing up to store senders’ outbound bodies indefinitely, on a surface browsers can write to. Treat it accordingly: set the daemon’s auth_token (the pin URL settings take a bearer token) or put network isolation in front, mind the max_pin_bytes cap, and own the retention story — sweeping stale client pins is your policy, because no protocol sweep will do it.

The live proof

webclients/shared/test/e2e-noapi.test.js is this chapter runnable: the seeded dev validator plus one sithbit-ipfsd, no account-api and no mail server, driving the real wasm bundle through enumeration, the postage affordance, a client-pinned send, and the recipient’s trustless read-back. The env-gated runbook is in the file header.