Adoption
Do I have to remove passwords?question
No, and you probably should not, at least not at first. Passkeys work perfectly well alongside a password form, and running both is how most sites roll them out. See adding to an existing app for the staged path.
What happens if a user loses every device?question
They lose the account, unless you gave them a recovery path. This is the hardest part of going passwordless and no library can solve it for you.
The cheapest mitigation by a wide margin is prompting for a second passkey at signup, on a different device. See account recovery.
Will this work for users who have never heard of passkeys?question
The prompt is the operating system's own, and it says "Touch ID" or "Windows Hello" rather than anything about cryptography. Most users experience it as "sign in with my fingerprint".
The part that confuses people is the first time, when the OS asks whether to save a passkey. A one-line explanation next to your button helps more than any amount of in-prompt wording you cannot control.
Is passkify production ready?question
It implements every check in WebAuthn §7.1 and §7.2, and the test suite drives a virtual authenticator that assembles authenticator data byte by byte and signs with real keys, so each check is exercised against a tampered response rather than just the happy path.
It has not had an external security audit. If that matters for your risk profile, read the security model, read the source, and decide.
Comparisons
How is this different from a hosted auth provider?question
A hosted provider (Clerk, Auth0, WorkOS and similar) runs the identity system for you: sessions, recovery, admin UI, compliance. passkify is a library that verifies WebAuthn ceremonies against your own database, and nothing else.
If you want somebody else to own identity, use a provider. If you already have users, sessions and a login page, and you want passkeys added to them, that is what this is for.
Why not use a WebAuthn library that already exists?question
Existing libraries are good. The differences here are deliberate rather than competitive:
- Client and server in one package, so the encoding on both sides is written by the same code and cannot disagree.
- Zero runtime dependencies. The parser sits on attacker-controlled bytes, and every dependency in that path is code you trust without reading.
- Failure throws. There is no
verified: falseto accidentally treat as success. - An API that refuses to express the account-takeover bug. Registration cannot be pointed at an existing account from a request body.
Behaviour
Why does login() not tell me the username does not exist?question
Because that would make your login form an account-enumeration oracle: anyone could learn which usernames are registered by watching for a 404.
An unknown username gets a normal, well-formed challenge, and the ceremony fails at verification like any other bad attempt. See authentication.
Can one passkey work across my subdomains?question
Yes, if rpID is set to the shared parent domain before the first user
registers. rpID: 'acme.com' covers app.acme.com and www.acme.com.
You cannot change it afterwards. Credentials are permanently bound to the
rpID they were created under, and changing it orphans every one of them.
Can one account have several passkeys?question
Yes, and it should. Each device registers its own. excludeCredentials stops
the same authenticator registering twice, and
listCredentials gives you the settings page.
Does passkify support 2FA, where a passkey is the second factor?question
Yes. Run a normal authentication ceremony after your password check, scoped to the account you just verified:
const { options } = await passkeys.startAuthentication({
userId: userFromPasswordCheck.passkeyHandle,
userVerification: 'discouraged', // presence only; the password was factor one
});What about hardware security keys?question
They work. They register as deviceType: 'singleDevice' because they cannot
sync, which matters only for your recovery planning.
Do not set
requireBackupEligible
unless you intend to turn those users away.
Operations
Do I need Redis?question
No. Any store that can do an atomic fetch-and-delete works, including a plain
SQL table with DELETE ... RETURNING.
Redis is a good fit for challenges specifically, because they are short-lived,
high-churn, and GETDEL gives the strongest replay guarantee in one round trip.
See store adapters.
Can I run this on an edge runtime?question
Both halves, yes. The verification path is pure WebCrypto, so nothing on a
ceremony path imports a node: built-in — a test walks the source on every run
and fails if one does. Cloudflare Workers, Deno and Vercel Edge need no
compatibility flag.
How do I migrate credentials from another library?question
You need the raw COSE public key bytes per credential, plus the credential ID,
algorithm, counter and the user handle already on the device. rpID must be
identical.
If your previous library stored PEM or DER instead of COSE, the conversion is not reversible and users have to re-enrol. Check that before planning the migration. See migrating.
How much does this add to my bundle?question
About 3.6 kB gzipped for passkify/client — 10.3 kB before compression. The
figure on the home page is measured from source on every docs build rather
than typed in here.
Import the subpath, not the package root. The root is the server half, and since verification moved to WebCrypto it bundles for a browser without erroring — so importing it costs you about six times the size and nothing warns you.
Still stuck
Troubleshooting covers the specific failures,
symptom by symptom. Nearly every remaining case is a disagreement between the
configured rpID, the configured origin, and the origin the browser actually
reports.