What is a passkey, and which of the two kinds you just got
A passkey is a key pair your user's device holds and proves on your domain. A synced one and a device-bound one behave differently the day a phone dies, and the ceremony tells you which you got.
In short
A passkey is a public-key credential: the user's device or password manager keeps the private half, your server keeps the public half, and signing in means the device signs a challenge your domain issued. Two kinds ship under that one name, synced and device-bound, and only one of them survives a lost phone.
A passkey is a public-key credential. The user's device or password manager keeps the private half, your server keeps the public half, and signing in means the device signs a fresh challenge for your domain once the user confirms with a fingerprint, a face or a PIN.
Every explainer on this query says that, and it is right. It also stops one step short of the thing that decides how your support queue looks in a year.
Two different credentials ship under the word passkey. One is synced through a credential manager and lands on every device the user owns. The other is stuck on the single device that made it. Same API, same ceremony, same row in your database. Completely different answer to "my phone went in the river".
And the ceremony tells you which one you just got. Most implementations throw that away.
Why they exist
Because a shared secret has two owners and either one can lose it. The user can hand a password to a convincing lookalike page, and you can lose the whole table to a breach, however carefully you hashed it.
A passkey removes the shared secret. There is nothing on your server worth stealing, because the public half is useless on its own, and there is nothing to phish, because the credential only answers on the domain it was created for. A lookalike site at a different hostname does not get a prompt. It gets silence.
That is the whole security argument, and it is the part already written about everywhere. The rest of this page is about what you actually hold afterwards.
How it actually works
Four things happen, and only the middle two are interesting.
Your server generates a random challenge and names the domain the credential will belong to. The browser passes both to whatever credential manager the user has. That manager mints a key pair scoped to that one domain, asks the user to confirm, and hands back the public half with a signature over the challenge for your server to check. Then it stores what came back.
The domain part is not a detail. WebAuthn binds every credential to the hostname
that ran the ceremony, which on BuildBase is your organization's auth domain
rather than your app or your API. Change that hostname later and the browser
stops offering the passkeys registered under the old one, because as far as
WebAuthn is concerned they belong to a different site. We store the rpId on
every passkey and compare it to the current auth domain, so a credential
stranded by a domain move is at least visible rather than mysteriously absent.
Settle the domain before anyone enrols, which is the argument
running auth on your own domain
makes at greater length.
The challenge has a clock on it. Ours is held in Redis for 300 seconds, 5 minutes exactly, and is consumed atomically, so it answers once and once only. A user who starts signing in, wanders off and comes back gets an expired-challenge error instead of a replayable window. They try again. That is the whole cost of the short window, and it is why the window is short.
Here is what the server ends up holding, which is less than people expect:
| Stored | What it is |
|---|---|
| Credential id | The handle the browser uses to find the right key |
| Public key | The half that verifies signatures and decrypts nothing |
| Signature counter | A number the authenticator increments, if it bothers |
| Transports | How the authenticator was reached: internal, USB, NFC |
| Device type, backup | Which of the two kinds this is, and whether it is saved |
| Relying party id | The hostname this credential will answer on |
No secret. Nothing that signs in anywhere else. If someone walks off with that table, they have a list of public keys and the knowledge that your users exist.
The two kinds, and the flag that names them
This is the part the generic explainers skip.
The authenticator reports two things about backup when it creates a credential. Backup eligibility says whether this credential is allowed to leave the device it was born on. Backup state says whether it currently has. The W3C WebAuthn specification is blunt about the difference in lifetime: backup eligibility "is a credential property and is permanent for a given public key credential source", while backup state "can change over time".
So:
| Eligible | Backed up | What you have |
|---|---|---|
| Yes | Yes | A synced passkey, on every device in that manager |
| Yes | No | Syncable, not yet saved anywhere else |
| No | No | Device-bound. This device or nothing |
Read the middle row again, because it is the one that bites. The user has a passkey that is allowed to sync and has not synced yet. They look identical to the synced case in your UI, right up until the device is gone.
Key takeaway
Backup eligibility is permanent and tells you what the credential can ever do. Backup state is a snapshot and tells you what it has done so far. Storing only the second one, at registration, answers the wrong question later.
We store both at registration, and the honest caveat is that we store the backup state as it stood that day. Our login path updates the signature counter and the last-used time, and does not refresh the backup flag. The specification says that flag appears in the data from any ceremony, registration and sign-in alike, so a live value is there to be read on every login if you want one. If you are building recovery prompts off a stored backup flag, know which it is: a snapshot from enrolment day, or something you keep current.
There is a second flag worth the same attention. A passkey ceremony does not always verify the user. It can check that somebody is present, a touch on the authenticator, without checking who. A passkey sign-in that verified the user with a PIN or a biometric counts as both steps on its own, which is why we pass that result through to the login gate rather than assuming it. A ceremony that only proved presence proves the device, and no more.
When a passkey is the wrong choice
An explainer that only lists upsides is a brochure with a dictionary voice, so:
As the only way in. Whatever lets a user back without their passkey becomes the real security boundary of the account. If that is an emailed link, your passkeys are as strong as an inbox. Not a reason to skip them. A reason to stop calling the recovery path an edge case and go look at it.
For accounts people share. A passkey is bound to a credential manager, which belongs to a person. Two contractors and one logistics login do not fit that shape, and the workaround users invent will be worse than whatever you were avoiding.
On machines nobody owns. Shared terminals, kiosks, a borrowed laptop. A passkey there is either unusable or gets enrolled somewhere it should not live.
Before you have settled your auth hostname. Covered above, and it is the expensive one, because it is invisible until the day you move and then it is every user at once.
None of these argue for passwords. They argue for passkeys plus one other method that does not depend on the same device, which is how the sign-in list is built anyway: passkeys sit alongside email codes, magic links and the rest, and a user who cannot reach one reaches another.
Where it sits next to the others
Passkeys are one of the passwordless methods, not the whole category, and the others trade differently. A magic link asks nothing of the device and everything of the inbox: no enrolment, no lost-phone story, and a session handed to whoever is reading the mail. A passkey inverts that. It asks for enrolment on a real device and gives you a credential that phishing does not touch.
The practical answer for most products is both, with the passkey as the fast path and something email-shaped underneath it. If you want the implementation rather than the concept, adding passkeys in Next.js has the decision that actually costs you something, which is the same hostname question as above.