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.
Passkeys are disabled by default. Enabling them has two gates — a build-time flag and a runtime flag — because the passkey database table must be generated ahead of time and can't depend on a runtime environment variable.
Set ENABLE_PASSKEY to true in packages/better-auth/src/auth.features.ts:
export const ENABLE_PASSKEY = true;
This constant is read by both the runtime auth config (auth.ts) and the schema-generation entrypoint (config.ts). When false, the passkey plugin is not registered server-side at all — the /passkey/* endpoints don't exist, and the passkey table is not generated. Disabling is a real boundary, not just a hidden UI.
With the flag on, generate the Better Auth schema (now including the passkey table) and apply the Prisma migration:
pnpm --filter @kit/better-auth schema:generate pnpm --filter @kit/database prisma migrate dev --name add-passkey
Set the public flag so the sign-in button and settings card render:
NEXT_PUBLIC_AUTH_PASSKEY=true
When all three steps are done:
A passkey is bound to an existing account, so users register one after they already have an account:
NEXT_PUBLIC_SITE_URL. In development this is localhost; in production it must be your real domain (e.g. app.example.com). A mismatch causes the browser to reject the passkey.ENABLE_PASSKEY enables the server plugin and table; NEXT_PUBLIC_AUTH_PASSKEY enables the UI. With only the UI flag on, the buttons render but the endpoints return errors. With only the build flag on, the feature works but is never surfaced to users.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.
Good for:
Consider alternatives when: