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

Passkey login

UX principles

  • Be transparent about the trust model. Users should know up front that the passkey is the wallet, not just a convenience layer.
  • Make passkey login a choice on supported devices, not a forced default. Users who prefer mnemonic onboarding should be able to opt out without friction.

Guidelines

  1. Gate passkey UI on availability. Call PasskeyClient.check_availabilityPasskeyClient.check_availabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.CheckAvailabilityPasskeyClient.CheckAvailability at startup and fall back to mnemonic onboarding on unsupported devices. The same call surfaces domain-association failures, so one check covers both "device can't" and "config is broken".
  2. Match the CTA layout to the platform. iOS and Android: a single primary CTA backed by PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey (silent sign-in for returning users, fall-through to register for new ones). Web: two CTAs, "Create a new passkey" and "Sign in with a passkey", since WebAuthn can't tell "no credential" from "cancel" for auto-detection.
  3. Don't add your own consent screen. The OS shows its own consent UI, so don't gate registration behind a separate "I understand" review step.
  4. Cache the derived seed within a session. A sign-in or register call avoids re-prompting for later PasskeyLabels.listPasskeyLabels.listPasskeyLabels.listPasskeyLabels.listPasskeyLabels.listPasskeyLabels.listPasskeyLabels.listPasskeyLabels.ListPasskeyLabels.List / PasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.StorePasskeyLabels.Store calls in the same session. For reuse across an app relaunch, keep the seed in your own in-memory cache and pass it to connectconnectconnectconnectconnectconnectconnectConnectConnect; never persist it to disk.
  5. Never persist the derived mnemonic. Re-derive it from the passkey and label on each session. Persisting it would bypass the OS authentication prompt.
  6. Allow manual mnemonic backup. Offer a user-initiated "Show recovery phrase" path that derives the mnemonic on demand via PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn, so users keep a recovery option if they lose the passkey.
  7. Match on PrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderError variants. They are the cross-language error surface. Rust callers can branch on the collapsed error.kind() instead.
  8. Don't auto-retry on dismissed prompts. The SDK never re-fires the OS prompt on its own. A dismissed prompt should lead to a sticky error screen with a "Try Again" button; only a user tap retries.

Onboarding flow

Browsers and native authenticators expose different error semantics, so the recommended UX differs by platform.

iOS 18+ / Android 9+: one "Use Passkey" button backed by PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey. It tries a silent sign-in first: a returning user gets a single biometric prompt; a new user fast-fails with no UI and the SDK falls through to register. On a real cancel, show a sticky retry and do not auto-register.

PathOS prompts
Returning user1 (one assertion derives master + label)
New user2 (1 create, 1 assertion)

Web: two buttons, "Create a new passkey" and "Sign in with a passkey". WebAuthn reports "no credential" and "user cancelled" identically, so the SDK can't auto-detect which the user wants. PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey is not surfaced on the WASM target.

See Onboarding for the call shapes.

Adding a wallet under a new label

For a user who already has a passkey and wants another wallet:

  1. PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn with the new label. One assertion derives the master and new-label seeds.
  2. PasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.storePasskeyLabels.StorePasskeyLabels.Store to publish the label to Nostr. This reuses the cached identity, so it adds no prompt.

Total: 1 OS prompt. Storing the label first would cost 2, since each call would derive the master salt independently.

Credential metadata

Every flow returns a PasskeyCredentialPasskeyCredentialPasskeyCredentialPasskeyCredentialPasskeyCredentialPasskeyCredentialPasskeyCredentialPasskeyCredentialPasskeyCredential: the credential ID on every path, plus the user handle, AAGUID, and backup flag on registration. Persist them to keep a returning user on the same wallet, block duplicate registrations on one device, and show which authenticator holds the passkey and whether it syncs. AAGUID and the backup flag are unverified: treat them as display hints, never trust signals.

See Credential metadata for the fields, where to store them, and the per-use-case code.

Recovery paths

Every passkey failure normalizes to a PrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderError variant. See Onboarding error recovery for the variant-to-action table; the guidelines above cover the UX rules.

On iOS, the SDK disambiguates the platform's generic failure (missing credential, cancel, or timeout) into these variants for you, so you don't need a host-side timer.