threshold1's flagship method — platform authenticators (Touch ID, Windows Hello, a phone's biometric unlock) via WebAuthn. No password, no code to enter, no email round-trip.
Before any passkey call will succeed, your project needs a domain configured — this is the one real prerequisite, and it's covered in full on the Passkey-first & smart fallback page: test_domain for a th_test_ key, primary_domain for a th_live_ key, and why localhost categorically can't be used as either. Don't skip that page before wiring up passkey — nothing below will work without it.
// As part of the cascade — tries passkey first automatically.
await auth.register("user@example.com");
// Or add one to an already-authenticated user.
await auth.addPasskey({ externalUserId: "your_user_123", email: "user@example.com" });
addPasskey() requires an active session — call it after any other successful login. Its externalUserId/email follow the identity-bridge rules covered on the External User ID Bridging page: externalUserId attaches only if the user doesn't already have a different one, email only fills a gap. There's no cap on how many passkeys a single user can register.
// Explicit email — tries passkey first, falls back per auth.fallback.
await auth.login("user@example.com");
// No email — discoverable login, browser shows a passkey picker.
await auth.login();
The no-email path is the three-state model covered on the fallback page — that page is the right place to understand what happens when this fails, not this one.
const passkeys = await auth.listPasskeys();
// [{ credentialId, createdAt }]
await auth.removePasskey(passkeys[0].credentialId);
Both require an active session and only ever operate on the current user's own passkeys.
These are fixed, not currently configurable:
passkey/*/start must be completed within 10 minutes or it expires and you'll need to restart the ceremony.authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" } — resident keys (discoverable credentials, needed for the no-email login path) are requested but not required; the same for user verification (biometric/PIN). An authenticator that can't satisfy either still works, just without those properties.supportedAlgorithmIDs: [-7, -257]) — covers essentially every platform authenticator in real-world use.Passkey routes can return invalid_code (verification failed), expired (challenge TTL passed), user_exists (registering an email that already has an account), and user_disabled. See the Error Reference for what each means and how to handle it.
addPasskey()'s identity-linking rules work.