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 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 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"). 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.

CLI#

Add the following to supabase/config.toml:

1
[auth.passkey]
2
enabled = true
3
4
[auth.webauthn]
5
rp_display_name = "My App"
6
rp_id = "example.com"
7
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:

1
# Get your access token from https://supabase.com/dashboard/account/tokens
2
export SUPABASE_ACCESS_TOKEN="your-access-token"
3
export PROJECT_REF="your-project-ref"
4
5
# Read the current passkey configuration
6
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
7
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
8
| jq '{passkey_enabled, webauthn_rp_id, webauthn_rp_display_name, webauthn_rp_origins}'
9
10
# Enable passkeys and set the WebAuthn relying party
11
curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
12
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
13
-H "Content-Type: application/json" \
14
-d '{
15
"passkey_enabled": true,
16
"webauthn_rp_display_name": "My App",
17
"webauthn_rp_id": "example.com",
18
"webauthn_rp_origins": "https://example.com,https://app.example.com"
19
}'

Enable in the client#

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabase = createClient(supabaseUrl, supabaseKey, {
4
auth: {
5
experimental: { passkey: true },
6
},
7
})

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.

1
const { data, error } = await supabase.auth.registerPasskey()
2
3
if (error) {
4
// User cancelled, browser doesn't support WebAuthn, or verification failed
5
console.error(error)
6
} else {
7
console.log('Registered passkey', data.id)
8
}

The returned data contains the new passkey's metadata:

1
{
2
id: string // UUID — use this to update or delete the passkey
3
friendly_name?: string // Derived from the authenticator's AAGUID
4
created_at: string
5
}

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

1
const { data, error } = await supabase.auth.signInWithPasskey()
2
3
if (error) {
4
console.error(error)
5
} else {
6
// data.session and data.user are set; the client also dispatches a SIGNED_IN event
7
console.log('Signed in as', data.user?.email)
8
}

See the signInWithPasskey reference 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:

1
const { data: options } = await supabase.auth.passkey.startRegistration()
2
// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library)
3
const credential = await runRegistrationCeremony(options.options)
4
await supabase.auth.passkey.verifyRegistration({
5
challengeId: options.challenge_id,
6
credential,
7
})

Authentication:

1
const { data: options } = await supabase.auth.passkey.startAuthentication()
2
// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library)
3
const credential = await runAuthenticationCeremony(options.options)
4
const { data } = await supabase.auth.passkey.verifyAuthentication({
5
challengeId: options.challenge_id,
6
credential,
7
})

The options field returned from startRegistration and startAuthentication matches the WebAuthn PublicKeyCredentialCreationOptions and PublicKeyCredentialRequestOptions shapes (with ArrayBuffer fields encoded as base64url).

See the auth.passkey reference for the full API.

Manage passkeys#

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

1
// List
2
const { data: passkeys } = await supabase.auth.passkey.list()
3
// [{ id, friendly_name, created_at, last_used_at? }, ...]
4
5
// Rename
6
await supabase.auth.passkey.update({
7
passkeyId: passkeys[0].id,
8
friendlyName: 'Work laptop',
9
})
10
11
// Delete
12
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 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.

1
import { createClient } from '@supabase/supabase-js'
2
3
const supabase = createClient(supabaseUrl, supabaseSecretKey, {
4
auth: { experimental: { passkey: true } },
5
})
6
7
const { data } = await supabase.auth.admin.passkey.listPasskeys({ userId })
8
9
await supabase.auth.admin.passkey.deletePasskey({ userId, passkeyId })

See the auth.admin.passkey reference for the full API.

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.