Passkeys
Passwordless, phishing-resistant authentication with passkeys (WebAuthn).
Passkeys let users sign in with their device's biometrics (Face ID, Touch ID, Windows Hello) or a hardware security key instead of a password. They are passwordless and phishing-resistant by design.
Enable Passkeys
Passkeys are disabled by default. Because they add a passkey database table, enabling them is a provisioning decision in two parts — not a single env flag.
1. Provision the table and server plugin
Flip the committed constant — the single source of truth shared by schema generation (config.ts) and the runtime (auth.ts):
export const ENABLE_PASSKEY = true;
Then regenerate the schema and apply the migration:
pnpm --filter @kit/better-auth schema:generate pnpm --filter @kit/database drizzle:generate pnpm --filter @kit/database drizzle:migrate
While ENABLE_PASSKEY is false, the passkey plugin is not registered server-side, so the /passkey/* endpoints don't exist — disabling is a real boundary, not just a hidden UI.
2. Show passkeys in the UI
NEXT_PUBLIC_AUTH_PASSKEY=true
With both set:
- A "Sign in with a passkey" button appears on the sign-in page, alongside the other enabled methods.
- A Passkeys card appears under Settings → Security, where signed-in users register, view, and remove passkeys.
NEXT_PUBLIC_AUTH_PASSKEY=true only controls the UI. Setting it without ENABLE_PASSKEY=true (and the migration applied) renders a passkey button whose endpoints don't exist. Always provision first.
How It Works
A passkey is bound to an existing account, so users register one after they already have an account:
- User signs up / signs in with another method (email & password, magic link, OAuth)
- User opens Settings → Security and clicks Add a passkey
- The browser prompts to create the passkey using biometrics or a security key
- On the next visit, the user clicks Sign in with a passkey and authenticates with their device
Configuration Notes
- Relying party domain — the passkey is tied to your site's domain, derived from
NEXT_PUBLIC_SITE_URL. In development this islocalhost; in production it must be your real domain (e.g.app.example.com). A mismatch causes the browser to reject the passkey. - Database — enabling passkeys adds a
passkeytable, provisioned by settingENABLE_PASSKEYand regenerating the schema. See Enable Passkeys for the full sequence.
Client Usage
import { authClient } from '@kit/better-auth/client';
// While signed in: register a passkey
await authClient.passkey.addPasskey({ name: 'MacBook Touch ID' });
// List registered passkeys
const { data: passkeys } = await authClient.passkey.listUserPasskeys();
// Remove a passkey
await authClient.passkey.deletePasskey({ id: passkeyId });
// On a later visit: sign in with a passkey
await authClient.signIn.passkey();
The kit also ships hooks under @kit/auth/hooks: use-add-passkey, use-list-passkeys, use-delete-passkey, and use-sign-in-with-passkey.
When to Use Passkeys
Good for:
- Phishing-resistant, passwordless sign-in
- Users on modern devices with biometrics
- Reducing password-reset support load
Consider alternatives when:
- Users are on shared or older devices without authenticators
- You need a sign-in method available before account creation (passkeys are registered after sign-up)