Passwords are the slowest moving disaster in software. They get phished, reused across sites, dumped in breaches, and forgotten by users who then call support. We have known this for twenty years, and yet most apps still ship a password field as the front door.
Passkeys are the first credential primitive that actually fixes the root cause. They are unphishable, unique to each site, and synced across a user's devices by their OS. This post is a developer's walkthrough of what a passkey is, how the WebAuthn protocol works, and how to ship the server side with `@simplewebauthn/server`.
What a passkey actually is
A passkey is a public/private key pair, scoped to a single domain (the Relying Party ID), stored in the user's authenticator. The authenticator can be:
- A platform authenticator (Touch ID, Face ID, Windows Hello, Android biometrics).
- A roaming authenticator (a YubiKey or similar).
- A synced credential (iCloud Keychain, Google Password Manager, 1Password).
The public key lives on your server. The private key never leaves the authenticator. Authentication is a signature over a server challenge, verified with the stored public key.
Three properties fall out of that design:
- Phishing resistant. The browser will only sign challenges for the exact domain that registered the credential. A lookalike domain gets nothing.
- No shared secret to leak. A database breach gives the attacker public keys, which are useless on their own.
- No password to forget. The biometric prompt is local to the device. Recovery is handled by the user's OS or password manager.
The protocol underneath: WebAuthn and FIDO2
Passkey is the consumer brand. The standards underneath are:
- WebAuthn: the W3C browser API. `navigator.credentials.create()` and `navigator.credentials.get()`.
- CTAP2: the protocol the browser speaks to the authenticator (USB, NFC, BLE, or platform).
- FIDO2: the umbrella that includes both.
Your code only ever touches WebAuthn. The browser handles CTAP2 for you.
Setup: server identity and origin
Before any flow, decide two values and never let them drift between client and server:
The `rpID` must match the registrable domain. If you serve from `app.blockwill.io`, you can set `rpID` to `blockwill.io` so passkeys work across subdomains. Get this wrong on day one and you cannot change it later without invalidating every credential.
Registration flow
Four steps. Two on the server, two on the client.
Step 1: server generates options
`residentKey: "required"` is what makes it a true passkey (a discoverable credential). `excludeCredentials` prevents the user registering the same authenticator twice. The challenge is stored server side with a 5 minute TTL.
Step 2: client invokes the browser API
The browser shows a native prompt. The user approves with biometrics. Done.
Step 3: server verifies and stores
Key points:
- `expectedOrigin` and `expectedRPID` must be exact. A typo here turns the whole flow into a silent failure.
- Store the public key as binary, not as a base64 string you re-encode every read.
- Delete the challenge after use so it cannot be replayed.
Authentication flow
Same shape, different verbs.
Step 1: server generates a challenge
Leaving `allowCredentials` empty unlocks the killer UX feature: the browser shows a list of passkeys the user already has for your site, and they pick one. No email entry, no password field.
Step 2: client signs the challenge
Step 3: server verifies the signature
The `counter` is a replay protection signal. Synced passkeys (iCloud, Google) often return `0` because they live in the cloud rather than on a single device. Do not hard reject when `newCounter <= counter`. Instead, log it and decide based on whether the credential is synced.
UX patterns that actually work
- Conditional UI. Add `autocomplete="username webauthn"` to your email field. The browser surfaces saved passkeys inline as the user clicks the field. No separate "sign in with passkey" button needed.
- Progressive enrolment. Do not block sign up on passkey creation. Let users register with email + password, then offer passkey on first successful login.
- Always allow multiple credentials. A user with an iPhone and a Windows laptop will register both. Show them a list with names like "iPhone (added Jan 2026)".
- Recovery is a real feature. Build a recovery email or backup codes flow before you ship passkeys, not after the first lost-device support ticket.
Migration: how to ship without breaking the flow
The sane migration path on an existing app:
- Keep password login working. Do not remove anything yet.
- Add a "Set up passkey" CTA in account settings. Offer it after a successful password login.
- Track the percentage of active users with at least one passkey.
- Once the number is high enough (most teams target 60 to 80 percent), move passkey to the primary login screen and demote password to a "Use password instead" link.
- Years later, consider whether you can deprecate passwords for active users. Most teams will never get there, and that is fine. Coexistence is a stable end state.
FAQ
Are passkeys really phishing proof?
Yes, against credential phishing specifically. The browser binds the signature to the origin that registered the credential, so a fake `b1ockwill.io` page cannot trick the authenticator into signing for `blockwill.io`. Passkeys do not stop social engineering that asks the user to disable security or share their screen, and they do not stop session hijacking after login. Defence in depth still applies.
What happens when a user loses their device?
It depends on whether the passkey was synced. iCloud Keychain, Google Password Manager, and 1Password sync passkeys across the user's devices. If the user signs in to a new device with the same Apple, Google, or 1Password account, the passkey comes with them. For non synced credentials (some YubiKeys, older platform authenticators), you need a recovery flow: backup codes, recovery email, or a second passkey on a different device.
Do I still need passwords as a fallback?
For most consumer apps, yes, for at least the next few years. Some users do not have a synced password manager, some do not understand the prompt, and some accounts predate the passkey rollout. Treat password as a legacy fallback, not the primary path.
What about server side rate limiting?
WebAuthn does not remove the need for rate limits. An attacker can still hammer your `verifyAuth` endpoint with garbage assertions to probe for valid `credentialId` values or to cause CPU load with signature verification. Rate limit by IP and by `credentialId`.
How do passkeys compare to TOTP (authenticator apps)?
TOTP is a 30 second rotating code. It is shared secret based, so phishable: a user can be tricked into typing the code into a fake site. Passkeys are public key signatures bound to the origin, so phishing returns nothing useful. Passkeys are strictly stronger.
Is `attestationType: "none"` safe?
For consumer auth, yes. Attestation tells you the make and model of the authenticator. Banks and government services may need it to enforce hardware policies. Most apps do not, and asking for it triggers extra prompts and incompatibility with synced passkeys.
What do I do about the `counter` always being zero?
Synced passkeys return zero because no single device owns the credential. The `@simplewebauthn/server` library handles this gracefully. Do not write logic that rejects when `newCounter` does not increase. Trust the verifier and log anomalies for review rather than blocking the user.
Can I use passkeys for step up auth on sensitive actions?
Yes. Generate a fresh challenge for each sensitive operation (changing email, exporting data, transferring funds) and require the user to sign it. This is called WebAuthn step up and is one of the strongest patterns available for high value flows.
What to take away
- Passkeys are public key pairs scoped to your domain. The browser does the cryptography. You verify signatures.
- WebAuthn is two flows (registration and authentication), four steps each, all wired up by `@simplewebauthn/server` and `@simplewebauthn/browser`.
- The hard parts are not the crypto. They are the UX, the recovery flow, the migration plan, and the multi credential model.
- Ship passkeys alongside passwords. Promote one and demote the other. Coexistence is a stable end state, not a transition.
Passwords were a workaround for not having user identity on the internet. Passkeys are the first credential that actually solves it. Worth the engineering investment.



