diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index 9c8f760b612..1fd5b0e51a8 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -755,6 +755,12 @@ export const auth: NavMenuConstant = {
enabled: allAuthProvidersEnabled,
},
+ {
+ name: 'Passkey',
+ url: '/guides/auth/passkeys',
+ enabled: allAuthProvidersEnabled,
+ },
+
{
name: 'Social Login (OAuth)',
url: '/guides/auth/social-login',
diff --git a/apps/docs/content/guides/auth/passkeys.mdx b/apps/docs/content/guides/auth/passkeys.mdx
new file mode 100644
index 00000000000..3165f7d4679
--- /dev/null
+++ b/apps/docs/content/guides/auth/passkeys.mdx
@@ -0,0 +1,244 @@
+---
+id: 'passkeys'
+title: 'Passkey authentication'
+subtitle: '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:
+{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */}
+
+- **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.
diff --git a/apps/docs/spec/cli_v1_config.yaml b/apps/docs/spec/cli_v1_config.yaml
index bb10e2fdf10..8dc6cb29e0b 100644
--- a/apps/docs/spec/cli_v1_config.yaml
+++ b/apps/docs/spec/cli_v1_config.yaml
@@ -1439,6 +1439,50 @@ parameters:
- name: 'Auth Multi-Factor Authentication'
link: 'https://supabase.com/docs/guides/auth/auth-mfa'
+ - id: 'auth.passkey.enabled'
+ title: 'auth.passkey.enabled'
+ tags: ['auth']
+ required: false
+ default: 'false'
+ description: |
+ Enable passkey sign-in and registration. Requires `auth.webauthn` to be configured.
+ links:
+ - name: 'Sign in with Passkey'
+ link: 'https://supabase.com/docs/guides/auth/passkeys'
+
+ - id: 'auth.webauthn.rp_display_name'
+ title: 'auth.webauthn.rp_display_name'
+ tags: ['auth']
+ required: false
+ default: ''
+ description: |
+ Human-readable name for the WebAuthn relying party shown to users during the passkey ceremony. Required when `auth.passkey.enabled` is `true`.
+ links:
+ - name: 'Sign in with Passkey'
+ link: 'https://supabase.com/docs/guides/auth/passkeys'
+
+ - id: 'auth.webauthn.rp_id'
+ title: 'auth.webauthn.rp_id'
+ tags: ['auth']
+ required: false
+ default: ''
+ description: |
+ WebAuthn relying party identifier. Must be a registrable suffix of every origin in `auth.webauthn.rp_origins` (for example `example.com` for `https://app.example.com`). Required when `auth.passkey.enabled` is `true`.
+ links:
+ - name: 'Sign in with Passkey'
+ link: 'https://supabase.com/docs/guides/auth/passkeys'
+
+ - id: 'auth.webauthn.rp_origins'
+ title: 'auth.webauthn.rp_origins'
+ tags: ['auth']
+ required: false
+ default: '[]'
+ description: |
+ List of origins your app is served from. The browser refuses the WebAuthn ceremony if the page origin is not in this list. Required when `auth.passkey.enabled` is `true`.
+ links:
+ - name: 'Sign in with Passkey'
+ link: 'https://supabase.com/docs/guides/auth/passkeys'
+
- id: 'auth.sessions.timebox'
title: 'auth.sessions.timebox'
tags: ['auth']
diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml
index 09cbdba4640..aab030e4956 100644
--- a/supa-mdx-lint/Rule003Spelling.toml
+++ b/supa-mdx-lint/Rule003Spelling.toml
@@ -22,6 +22,7 @@ allow_list = [
"[A-Z]{2,5}s?",
"[A-Za-z0-9_-]+(\\.[A-Z-a-z0-9_-]+)+(\\/[A-Za-z0-9_-]+)*",
"[Aa]dd-ons?",
+ "AAGUID",
"[Aa]llowlists?",
"[Aa]uditability",
"[Aa]utomations?",
@@ -34,7 +35,7 @@ allow_list = [
"BootFailure",
"[Bb]reakpoints?",
"[Bb]uilt-ins?",
- "[Cc]atalogs?",
+ "[Cc]atalogs?",
"[Cc]hangelogs?",
"CircleCI",
"[Cc]odebases?",
@@ -50,7 +51,7 @@ allow_list = [
"[Cc]ryptography",
"[Cc]ryptosystem",
"[Cc]utover",
- "[Dd]ata[Ff]rames?",
+ "[Dd]ata[Ff]rames?",
"[Dd]atasets?",
"[Dd]atasources?",
"[Dd]atetime",
@@ -59,7 +60,7 @@ allow_list = [
"[Dd]evs?",
"[Dd]iff(s|ing|ed)?",
"[Dd]ropdown",
- "DuckDB",
+ "DuckDB",
"EarlyDrop",
"[Ee]m-dash",
"[Ee]m-dashes",
@@ -115,6 +116,7 @@ allow_list = [
"[Oo]vercommit(s|ted|ting)?",
"[Pp]arallelization",
"[Pp]arams?",
+ "[Pp]asskeys?",
"[Pp]assthrough",
"[Pp]laintext",
"[Pp]olyfill(s|ed)?",
@@ -320,7 +322,7 @@ allow_list = [
"PGAudit",
"PGroonga",
"PgBouncer",
- "pgjwt",
+ "pgjwt",
"PHI",
"Pico",
"PingIdentity",