Appearance
Passkey Scope and Composability
Every Paskey WebAuthn credential is bound to a relying party ID (rpId), a domain that determines which sites can access the passkey and whether it works across subdomains.
The SDK supports two strategies:
Option A: Wallet-scoped (rpId = wallet domain) - One passkey reused across many apps. The wallet and app origins differ (e.g., app at app.example.com embedding wallet at web3authn.org with rpId = "web3authn.org"). This is the default mode, as it requires less setup.
Option B: App-scoped (rpId = app base domain) - Credentials bound to your product's domain, working across subdomains. The app and wallet share a base domain (e.g., wallet at wallet.example.com, app at app.example.com, with rpId = "example.com"). Choose this for single-product deployments. This requires contract deployment and configuration.
Both are secure. The key difference is passkey composability across apps on different domains (wallet-scoped) vs traditional domain binding (app-scoped).
Choosing your strategy
Wallet-scoped (rpId = wallet domain) - Choose when you want one passkey reused across many apps on different domains. Requires ROR configuration for Safari. Ideal for multi-tenant wallet services.
App-scoped (rpId = app base domain) - Choose when credentials should be tied to your product's domain. Simpler Safari integration, no ROR needed. Ideal for single-product deployments where the app and wallet share a base domain.
Option A: Wallet-scoped credentials
Use the wallet domain as rpId:
tsx
<PasskeyProvider
config={{
iframeWallet: {
walletOrigin: 'https://wallet.tatchi.xyz',
rpIdOverride: 'wallet.tatchi.xyz', // ← Wallet-scoped
}
}}
>The app's embedded wallet iframe calls WebAuthn with rpId = "wallet.tatchi.xyz".
Any app that embeds this iframe can reuse the same Passkey credential, as long as it delegates WebAuthn via Permissions-Policy.
Pros and Cons
Pros
- One passkey per user, reused across multiple apps
- Clear trust model: "if you trust the wallet origin, you can use this passkey"
- Good for multi-tenant wallet services
When the iframe calls WebAuthn directly:
https://app1.comembeds wallet iframe → reuses credentialhttps://app2.comembeds wallet iframe → reuses credentialhttps://totally-different.orgembeds wallet iframe → reuses credential
No allowlist needed! Any site can embed the wallet. Security relies on:
- The wallet's origin isolation
- The wallet's internal checks (verifying
clientDataJSON.origin) - The smart contract's verification
Key point: Wallet-scoped gives maximum reusability but requires careful ROR configuration specifically for Safari.
Safari limitations
- Safari blocks WebAuthn in cross-origin iframes, so the SDK falls back to requesting TouchID in the app origin causing the rpID to differ from the wallet origins.
- For Safari, this requires the app to be whitelisted (Related Origin Requests (ROR)) in the wallet domain's
/.well-known/webauthnendpoint
json
{
"origins": [
"https://wallet.tatchi.com",
"https://web3authn.org",
"https://example.your-app.com"
]
}The Wallet SDK automatically looks up the Web3Authn contracts get_allowed_origins endpoint and returns whitelisted origins, so every wallet deployment can call set_allowed_origins on the onchain Web3Authn contract and be added to the whitelist (or self-deploy their own wallet contracts).
Option B: App-scoped credentials
You can use your app's base domain as rpId when the wallet also lives under that base domain:
tsx
<PasskeyProvider
config={{
iframeWallet: {
walletOrigin: 'https://wallet.example.com',
rpIdOverride: 'example.com', // ← App-scoped
}
}}
>Both your app (https://app.example.com) and wallet (https://wallet.example.com) share the same registrable suffix, and credentials work across all *.example.com subdomains on all browsers (including Safari).
Pros and Cons
Pros
- Works naturally across your product's subdomains
- Safari's top-level bridge matches the top-level origin (no ROR needed)
- Traditional WebAuthn model
Cons
- Each product/domain needs its own credential
- No composability with wallets: cannot reuse passkey wallet across apps on different domains (e.g.,
social-app.comvsdefi-app.com) - If you switch to a different base domain later, old credentials won't appear
Configuration example
1. Set rpId to app base domain:
tsx
iframeWallet: {
walletOrigin: 'https://wallet.example.localhost',
rpIdOverride: 'example.localhost', // App base domain
}2. Enable Safari fallback:
Keep the top-level bridge enabled (it's on by default). The SDK will use it automatically when iframe WebAuthn fails.
3. Delegate WebAuthn:
text
Permissions-Policy:
publickey-credentials-get=(self "https://wallet.example.localhost"),
publickey-credentials-create=(self "https://wallet.example.localhost")Key takeaway: App-scoped gives predictable subdomain behavior with minimal Safari complexity.
Migrating wallet origins
To move the wallet host without losing passkeys, keep rpId stable:
tsx
iframeWallet: {
walletOrigin: 'https://wallet.tatchi.xyz', // New host
rpIdOverride: 'web3authn.org', // Old rpId (unchanged)
}Update ROR manifest on the rpId domain to include the new host, delegate WebAuthn to both origins during transition, then gradually migrate traffic. Credentials remain discoverable because they're bound to rpId, not the iframe host.
Testing your strategy
Browser compatibility:
- Chrome/Firefox: Both strategies work well
- Safari: Iframe WebAuthn often blocked, top-level bridge requires ROR for wallet-scoped
Verification checklist:
- Check ROR manifest:
curl https://<rpId>/.well-known/webauthn - Verify Permissions-Policy header in DevTools Network tab
- Inspect iframe has
allow="publickey-credentials-get; publickey-credentials-create" - Confirm passkey appears in browser password manager with correct
rpId
Common issues and fixes
"WebAuthn not available" in iframe
Cause: Permissions-Policy not set or iframe allow attribute missing.
Fix:
- Add header:
Permissions-Policy: publickey-credentials-get=(self "https://wallet.tatchi.xyz") - Ensure iframe has:
allow="publickey-credentials-get; publickey-credentials-create" - Check the SDK's Vite plugin is enabled
Passkey not appearing in Safari
Cause: ROR manifest missing or incorrect.
Fix:
- Serve
/.well-known/webauthnon therpIddomain - Include all top-level origins in the
originsarray - Verify with:
curl https://<rpId>/.well-known/webauthn - Clear Safari's passkey cache: Settings → Passwords → Remove test passkeys
Passkey works locally but not in production
Cause: Mismatched rpId configuration or HTTPS issues.
Fix:
- Verify
rpIdOverrideis set correctly in production config - Ensure all origins use HTTPS (WebAuthn requires secure context)
- Check production ROR manifest matches local configuration
- Verify Permissions-Policy header is sent in production
Cannot reuse passkey across apps
Cause: Wrong rpId strategy or ROR not configured.
Fix:
- For wallet-scoped: Ensure ROR manifest includes all app origins
- For app-scoped: Apps must share the same base domain
- Verify
rpIdOverrideis consistent across all apps
Next steps
- Learn about VRF-backed challenges
- Understand the architecture and iframe isolation model
- Review the security model