# Passkey authentication

Allow users to sign in with passkeys (WebAuthn)

[Passkeys](https://fidoalliance.org/passkeys/) are a passwordless credential built on the [WebAuthn](https://www.w3.org/TR/webauthn-3/) standard. The user proves possession of a private key stored on their device (or password manager) using biometrics, a PIN, or a hardware security key. The matching public key is registered with Supabase Auth and used to verify future sign-ins. Passkeys are phishing-resistant and remove the need to manage shared secrets.

Passkey support is experimental. The API may change without notice. You must explicitly opt-in when creating the Supabase client. See [Enable in the client](#enable-in-the-client).

## How does it work?

Each sign-in or registration is a WebAuthn ceremony with three steps:

1. **Options**: the client requests a challenge from Supabase Auth.
2. **Ceremony**: the browser invokes `navigator.credentials.create()` (registration) or `navigator.credentials.get()` (authentication), prompting the user for biometrics or a security key.
3. **Verify**: the signed response is sent back to Supabase Auth, which validates the challenge and either stores the new credential or issues a session.

Supabase Auth uses [discoverable credentials](https://www.w3.org/TR/webauthn-3/#discoverable-credential) for sign-in. The user does not need to provide an email, phone, or username — the authenticator resolves the account from the credential it stores.

Registering a passkey requires an existing, confirmed, non-anonymous user. Sign-in works for any user that has previously registered a passkey, provided their email or phone is confirmed and the account is not banned.

## Enable passkey authentication

### Dashboard

Open the [Passkeys settings](/dashboard/project/_/auth/passkeys) from the **Authentication → Passkeys** section of the Dashboard, turn on **Enable Passkey authentication**, and fill in the WebAuthn [relying party](https://www.w3.org/TR/webauthn-3/#relying-party) details:

- **Relying Party Display Name**: a human-readable name for your application shown during the passkey prompt (for example, "My App").
- **Relying Party ID**: the bare domain name for your application (for example, "example.com"). Do not include a scheme, port, or path. This determines which passkeys can be used.
- **Relying Party Origins**: comma-separated list of allowed origins (for example "https://example.com,https://app.example.com"). HTTPS is required except for loopback addresses ("localhost", "127.0.0.1", "[::1]"). Each origin's hostname must match or be a subdomain of the Relying Party ID. Up to 5 origins.

The dashboard pre-fills these from your project's Site URL and project name. Adjust them if your production app is served from a different domain.

Passkeys are cryptographically bound to the Relying Party (RP) ID they were registered against. Changing the RP ID makes every existing passkey unusable for sign-in, and users will need to register a new one. Pick the RP ID carefully before users start enrolling, and keep it stable once they do.

### CLI

Add the following to `supabase/config.toml`:

```toml
[auth.passkey]
enabled = true

[auth.webauthn]
rp_display_name = "My App"
rp_id = "example.com"
rp_origins = ["https://example.com", "https://app.example.com"]
```

The `[auth.webauthn]` section is required when `auth.passkey.enabled` is `true`.

### Management API

You can also configure passkeys via the [Management API](/docs/reference/api/introduction):

```bash
# Get your access token from https://supabase.com/dashboard/account/tokens
export SUPABASE_ACCESS_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"

# Read the current passkey configuration
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  | jq '{passkey_enabled, webauthn_rp_id, webauthn_rp_display_name, webauthn_rp_origins}'

# Enable passkeys and set the WebAuthn relying party
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "passkey_enabled": true,
    "webauthn_rp_display_name": "My App",
    "webauthn_rp_id": "example.com",
    "webauthn_rp_origins": "https://example.com,https://app.example.com"
  }'
```

## Enable in the client

Passkey support is currently experimental and requires explicit opt-in as the API may change without notice.

```ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(supabaseUrl, supabaseKey, {
  auth: {
    experimental: { passkey: true },
  },
})
```

## Register a passkey

A user must be signed in before they can register a passkey. Typically, you call this from a security settings page, or directly after sign-up.

`auth.registerPasskey()` runs the full WebAuthn ceremony. It fetches a challenge, invokes the browser API, and verifies the response with Supabase Auth.

```ts
const { data, error } = await supabase.auth.registerPasskey()

if (error) {
  // User cancelled, browser doesn't support WebAuthn, or verification failed
  console.error(error)
} else {
  console.log('Registered passkey', data.id)
}
```

The returned `data` contains the new passkey's metadata:

```ts
{
  id: string            // UUID — use this to update or delete the passkey
  friendly_name?: string // Derived from the authenticator's AAGUID
  created_at: string
}
```

A friendly name is automatically derived from the authenticator's Authenticator Attestation GUID (AAGUID). For example, `iCloud Keychain`, `Google Password Manager`, `1Password`. Users can rename their passkey afterwards — see [Manage passkeys](#manage-passkeys).

See the [`registerPasskey` reference](/docs/reference/javascript/auth-registerpasskey) for the full API.

## Sign in with a passkey

`auth.signInWithPasskey()` runs the full discoverable-credential authentication ceremony. The user picks an account from the authenticator's UI — your app does not need to ask for an email or phone number upfront.

```ts
const { data, error } = await supabase.auth.signInWithPasskey()

if (error) {
  console.error(error)
} else {
  // data.session and data.user are set; the client also dispatches a SIGNED_IN event
  console.log('Signed in as', data.user?.email)
}
```

See the [`signInWithPasskey` reference](/docs/reference/javascript/auth-signinwithpasskey) for the full API.

## Two-step API

For native flows, custom UI, or full control over the WebAuthn ceremony, use the lower-level `auth.passkey` namespace. Each operation is split into "start" and "verify".

Registration:

```ts
const { data: options } = await supabase.auth.passkey.startRegistration()
// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library)
const credential = await runRegistrationCeremony(options.options)
await supabase.auth.passkey.verifyRegistration({
  challengeId: options.challenge_id,
  credential,
})
```

Authentication:

```ts
const { data: options } = await supabase.auth.passkey.startAuthentication()
// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library)
const credential = await runAuthenticationCeremony(options.options)
const { data } = await supabase.auth.passkey.verifyAuthentication({
  challengeId: options.challenge_id,
  credential,
})
```

The `options` field returned from `startRegistration` and `startAuthentication` matches the [WebAuthn `PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions) and [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions) shapes (with `ArrayBuffer` fields encoded as base64url).

See the [`auth.passkey` reference](/docs/reference/javascript/auth-passkey-api) for the full API.

## Manage passkeys

List, rename, and delete the current user's passkeys:

```ts
// List
const { data: passkeys } = await supabase.auth.passkey.list()
// [{ id, friendly_name, created_at, last_used_at? }, ...]

// Rename
await supabase.auth.passkey.update({
  passkeyId: passkeys[0].id,
  friendlyName: 'Work laptop',
})

// Delete
await supabase.auth.passkey.delete({ passkeyId: passkeys[0].id })
```

`friendlyName` is limited to 120 characters. `last_used_at` is updated each time the passkey is used to sign in.

See the [`auth.passkey` reference](/docs/reference/javascript/auth-passkey-api) for the full API.

## Admin API

Server-side admin endpoints let you inspect and revoke a user's passkeys. These require the project's secret key and must only be called from a trusted server.

```ts
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(supabaseUrl, supabaseSecretKey, {
  auth: { experimental: { passkey: true } },
})

const { data } = await supabase.auth.admin.passkey.listPasskeys({ userId })

await supabase.auth.admin.passkey.deletePasskey({ userId, passkeyId })
```

See the [`auth.admin.passkey` reference](/docs/reference/javascript/auth-admin-passkey-api) for the full API.

## Error codes

| Code                            | Meaning                                                                          |
| ------------------------------- | -------------------------------------------------------------------------------- |
| `passkey_disabled`              | Passkey sign-in is not enabled for this project.                                 |
| `too_many_passkeys`             | The user has reached the maximum number of passkeys allowed per account.         |
| `webauthn_credential_exists`    | This authenticator has already been registered to the account.                   |
| `webauthn_credential_not_found` | The credential in the assertion is not registered with Supabase Auth.            |
| `webauthn_challenge_not_found`  | The challenge ID is unknown or has already been consumed.                        |
| `webauthn_challenge_expired`    | The challenge expired before the client returned a credential.                   |
| `webauthn_verification_failed`  | The signature, attestation, or assertion did not validate against the challenge. |

In addition, `signInWithPasskey()` returns the usual sign-in failure modes: `email_not_confirmed`, `phone_not_confirmed`, and `user_banned`.

## Limitations

- SSO users cannot register passkeys.
- Anonymous users cannot register passkeys — link an email or phone first.