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:

  1. User signs up / signs in with another method (email & password, magic link, OAuth)
  2. User opens Settings → Security and clicks Add a passkey
  3. The browser prompts to create the passkey using biometrics or a security key
  4. 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 is localhost; 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 passkey table, provisioned by setting ENABLE_PASSKEY and 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)