Skip to content
Auth

Passkey authentication

Allow users to sign in with passkeys (WebAuthn)

Passkeys are a passwordless credential built on the WebAuthn 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.

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 platform's passkey API (navigator.credentials.create() / get() on web, or a passkey plugin on iOS, Android, and macOS) prompts 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 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 from the Authentication → Passkeys section of the Dashboard, turn on Enable Passkey authentication, and fill in the WebAuthn 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"). Up to 5 origins.
    • 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.
    • Android native apps can use an app origin of the form android:apk-key-hash:<base64url SHA-256 of the signing certificate>.

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.

CLI#

Add the following to supabase/config.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:

# 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#

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 platform passkey API, and verifies the response with Supabase Auth.

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 passkey contains the new credential's metadata:

{
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.

See the registerPasskey reference (JavaScript · Dart · Swift) 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.

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 (JavaScript · Dart · Swift) 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:

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:

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 the start methods matches the WebAuthn PublicKeyCredentialCreationOptions and PublicKeyCredentialRequestOptions shapes (with ArrayBuffer fields encoded as base64url).

See the auth.passkey reference (JavaScript · Dart · Swift) for the full API.

Manage passkeys#

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

// 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. lastUsedAt is updated each time the passkey is used to sign in.

See the auth.passkey reference (JavaScript · Dart · Swift) 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.

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 (JavaScript · Dart) for the full API. The Swift SDK does not expose admin passkey methods.

Error codes#

CodeMeaning
passkey_disabledPasskey sign-in is not enabled for this project.
too_many_passkeysThe user has reached the maximum number of passkeys allowed per account.
webauthn_credential_existsThis authenticator has already been registered to the account.
webauthn_credential_not_foundThe credential in the assertion is not registered with Supabase Auth.
webauthn_challenge_not_foundThe challenge ID is unknown or has already been consumed.
webauthn_challenge_expiredThe challenge expired before the client returned a credential.
webauthn_verification_failedThe 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.