Passkeys are a secure, passwordless authentication method based on WebAuthn and FIDO2 standards. Users authenticate using biometrics (fingerprint, Face ID), a device PIN, or a hardware security key — no password required.
The passkey plugin is powered by SimpleWebAuthn under the hood.
Installation
Install the package
npm install @better-auth/passkey
Add the server plugin
Import passkey from @better-auth/passkey and add it to your plugins list:import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
export const auth = betterAuth({
plugins: [
passkey(),
],
});
Run the database migration
The passkey plugin needs a passkey table in your database: Add the client plugin
Import passkeyClient from @better-auth/passkey/client:import { createAuthClient } from "better-auth/client";
import { passkeyClient } from "@better-auth/passkey/client";
export const authClient = createAuthClient({
plugins: [
passkeyClient(),
],
});
Usage
Register a passkey
A user must be signed in before they can register a passkey. Call passkey.addPasskey:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.passkey.addPasskey({
name: "My MacBook", // optional label for this passkey
authenticatorAttachment: "platform", // "platform" or "cross-platform"
});
Sign in with a passkey
Call signIn.passkey to prompt the user to authenticate:
import { authClient } from "@/lib/auth-client";
await authClient.signIn.passkey({
fetchOptions: {
onSuccess(context) {
window.location.href = "/dashboard";
},
onError(context) {
console.error("Authentication failed:", context.error.message);
},
},
});
Browser autofill (Conditional UI)
Conditional UI lets the browser automatically suggest passkeys in input fields. Two things are required:
Add autocomplete attributes to inputs
Add webauthn as the last value of the autocomplete attribute on your input fields:<input type="text" name="username" autocomplete="username webauthn" />
<input type="password" name="password" autocomplete="current-password webauthn" />
Call signIn.passkey with autoFill on mount
import { useEffect } from "react";
import { authClient } from "@/lib/auth-client";
useEffect(() => {
if (
!PublicKeyCredential.isConditionalMediationAvailable ||
!PublicKeyCredential.isConditionalMediationAvailable()
) {
return;
}
void authClient.signIn.passkey({ autoFill: true });
}, []);
List passkeys
const { data: passkeys, error } = await authClient.passkey.listUserPasskeys();
Delete a passkey
await authClient.passkey.deletePasskey({
id: "passkey-id",
});
Update a passkey name
await authClient.passkey.updatePasskey({
id: "passkey-id",
name: "Work laptop",
});
Relying party configuration
Configure the relying party (RP) options in the passkey() plugin:
import { betterAuth } from "better-auth";
import { passkey } from "@better-auth/passkey";
export const auth = betterAuth({
plugins: [
passkey({
rpID: "example.com", // your domain (no protocol, no path)
rpName: "My App", // human-readable name shown in browser UI
origin: "https://example.com", // full origin URL, no trailing slash
authenticatorSelection: {
authenticatorAttachment: "platform", // "platform" | "cross-platform"
residentKey: "preferred", // "required" | "preferred" | "discouraged"
userVerification: "preferred", // "required" | "preferred" | "discouraged"
},
}),
],
});
| Option | Description |
|---|
rpID | Unique identifier for your site, based on the domain. localhost is valid for local development. |
rpName | Human-readable app name displayed in browser and OS prompts. |
origin | The origin URL where Better Auth is hosted. No trailing slash. |
authenticatorSelection | WebAuthn authenticator selection criteria. |
During local development you can omit rpID, rpName, and origin. Better Auth defaults to localhost.
Debugging
To test passkey registration and sign-in without a physical device, use Chrome’s emulated authenticators in DevTools.