# FAQ

Short answers to the questions that come up before and during adoption.

## Adoption

### Do I have to remove passwords?

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](/docs/guides/existing-app) for the staged
path.

### What happens if a user loses every device?

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](/docs/guides/security#account-recovery).

### Will this work for users who have never heard of passkeys?

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?

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](/docs/guides/security), read the source, and decide.

## Comparisons

### How is this different from a hosted auth provider?

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?

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: false` to 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?

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](/docs/server/authentication).

### Can one passkey work across my subdomains?

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?

Yes, and it should. Each device registers its own. `excludeCredentials` stops
the same authenticator registering twice, and
[`listCredentials`](/docs/server/credentials) gives you the settings page.

### Does passkify support 2FA, where a passkey is the second factor?

Yes. Run a normal authentication ceremony after your password check, scoped to
the account you just verified:

```js
const { options } = await passkeys.startAuthentication({
  userId: userFromPasswordCheck.passkeyHandle,
  userVerification: 'discouraged',   // presence only; the password was factor one
});
```

### What about hardware security keys?

They work. They register as `deviceType: 'singleDevice'` because they cannot
sync, which matters only for your recovery planning.

Do not set
[`requireBackupEligible`](/docs/server/configuration#requirebackupeligible)
unless you intend to turn those users away.

## Operations

### Do I need Redis?

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](/docs/storage/adapters#redis-for-challenges-only).

### Can I run this on an edge runtime?

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?

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](/docs/guides/existing-app#migrating-from-another-webauthn-library).

### How much does this add to my bundle?

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](/docs/guides/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.
