Skip to main content
The @portal-hq/web package on your page is a thin client. When you initialize Portal, it loads a hidden iframe from Portal (web.portalhq.io, or your custom subdomain). Key material and, depending on how you authenticate, Portal credentials live inside that iframe, on a different origin from your application. Your page sends requests to the iframe and receives results such as addresses, signatures, and transaction hashes.

Where things live

Signing is 2-of-2 MPC. The user’s signing share and Portal’s signing share must both take part, so neither Portal nor anything holding only your page’s data can sign alone.

How your page and the iframe communicate

  • All messages use window.postMessage with an explicit target origin in both directions.
  • The iframe is loaded with your page’s origin and only accepts messages from that origin and from its direct parent window.
  • The iframe only handles a fixed set of message types and ignores anything else.
  • The SDK only accepts replies that come from the Portal iframe’s own origin and window.
  • Iframe assets are published per SDK version, so each @portal-hq/web release loads the iframe built for it.

What origin isolation covers

Because the signing share and the session stay on the iframe origin, a cross-site scripting (XSS) bug on your page cannot read them.
Injected script on your page can still call SDK methods while the user’s session is active, for example to request a signature. Protect your application like any page that can move funds: set a strict Content Security Policy, load no untrusted scripts, and keep server-side checks on the transactions your users send.

Production checklist

  1. Keep Portal credentials out of your page. Use Client Auth or an authUrl. Passing a Client API Key or Client Session Token as apiKey exposes it to every script on your page, so reserve it for local development.
  2. Run the SDK on a custom subdomain. A custom subdomain such as portal.yourdomain.com keeps the iframe first-party, which Safari and other browsers with third-party cookie restrictions require.
  3. Set a Content Security Policy on your application. Allow the Portal iframe host (your custom subdomain or web.portalhq.io) in frame-src and connect-src. If you use passkey backups without the popup, also allow backup.web.portalhq.io in connect-src. The default passkey flow opens a popup on the Portal origin, so do not block it.
  4. Leave logging off in production. logLevel defaults to 'none'. Debug output includes request details meant for local troubleshooting. See Logging.
  5. Pin and update the SDK. Pin an exact @portal-hq/web version and upgrade deliberately.
  6. Approve signatures in your UI. Keep autoApprove set to false (the default) and handle portal_signingRequested to show users what they are signing. See Custom signature hooks. This runs in your page, so treat it as a user safeguard rather than an access control.
  7. Gate eject behind fresh authentication. portal.eject() rebuilds the full private key and returns it to your page. Only offer it on an explicit user request after the user re-authenticates.
  8. Prefer Firebase or Passkey backups over Password on web. With Password backups your page collects the password and passes it to the SDK. With Firebase backups and the default Passkey popup flow, the encryption key goes from the iframe to Portal’s Trustless Backup Service and never reaches your page. With a custom passkey domain, your page handles the key during backup, so keep that code path free of logging and third-party scripts.
  9. Only clear the wallet on purpose. clearLocalWallet() removes the signing share from the browser, after which the user needs to recover. For a normal sign-out, end your own session and leave the share in place.