# Build a custom authentication flow using passkeys (Legacy)

> This guide is for users who want to build a custom flow. To use a _prebuilt_ UI, use the [Account Portal pages](https://clerk.com/docs/guides/account-portal/overview.md) or [prebuilt components](https://clerk.com/docs/reference/components/overview.md).

> This guide uses the Core 2 `useSignIn()` and `useSignUp()` hooks, which are available in Core 3 SDKs by adding the `/legacy` subpath to the import path. If you're using a Core 2 SDK, remove the `/legacy` subpath.

Clerk supports passwordless authentication via [passkeys](https://clerk.com/docs/guides/configure/auth-strategies/sign-up-sign-in-options.md#passkeys), enabling users to sign in without having to remember a password. Instead, users select a passkey associated with their device, which they can use to authenticate themselves.

This guide demonstrates how to use the Clerk API to build a custom user interface for creating, signing users in with, and managing passkeys.

## Enable passkeys

To use passkeys, first enable the strategy in the [Clerk Dashboard](https://dashboard.clerk.com/~/user-authentication/user-and-authentication?user_auth_tab=passkeys).

### Domain restrictions for passkeys

Passkeys are tied to the domain they are created on and **cannot be used across different domains**. However, passkeys **do work on subdomains** if they are registered on the root domain. For example:

- Passkeys created on `your-domain.com` **cannot be used** on `your-domain-admin.com` (different domains).
- Passkeys created on `your-domain.com` **can be used** on `accounts.your-domain.com` (subdomain of the same root domain).
- Passkeys created on `staging1.your-domain.com` **cannot be used** on `staging2.your-domain.com` (sibling subdomains) unless the passkey was scoped to `your-domain.com` (i.e. the shared root domain).

**If you're using [satellite domains](https://clerk.com/docs/guides/dashboard/dns-domains/satellite-domains.md)**, in both development and production, passkeys won't be portable between your primary domain and your satellite domains so you should avoid using them.

If you're **not** using satellite domains:

- **In development**, you can either:
  - **The recommended approach**. Use Clerk's [components](https://clerk.com/docs/reference/components/overview.md), [Elements](https://clerk.com/docs/guides/customizing-clerk/elements/overview.md), or custom flows, instead of the [Account Portal](https://clerk.com/docs/guides/account-portal/overview.md). This ensures the passkey is created and used entirely on your development domain, so passkeys created on `localhost` will only work on `localhost`.
  - Create a passkey directly through the Account Portal instead of your local application to keep it tied to the Account Portal's domain. Passkeys created on your Account Portal (e.g., `your-app.accounts.dev`) will only work on that domain, which can cause issues if you switch between `localhost` and the Account Portal during development. If you choose this approach, ensure all testing happens on the same domain where the passkey was created.

- **In production,** your Account Portal is usually hosted on a subdomain of your main domain (e.g. `accounts.your-domain.com`), enabling passkeys to work seamlessly across your app. However, as stated above, if you use **satellite domains**, passkeys will not work as intended.

## Create user passkeys

To create a passkey for a user, you must call [User.createPasskey()](https://clerk.com/docs/reference/objects/user.md#create-passkey), as shown in the following example:

filename: components/CustomCreatePasskeysButton.tsx
```tsx
export function CreatePasskeyButton() {
  const { isSignedIn, user } = useUser()

  const createClerkPasskey = async () => {
    if (!isSignedIn) {
      // Handle signed out state
      return
    }

    try {
      await user?.createPasskey()
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
    }
  }

  return <button onClick={createClerkPasskey}>Create a passkey</button>
}
```

## Sign a user in with a passkey

To sign a user into your Clerk app with a passkey, you must call [SignIn.authenticateWithPasskey()](https://clerk.com/docs/reference/objects/sign-in.md#authenticate-with-passkey). This method allows users to choose from their discoverable passkeys, such as hardware keys or passkeys in password managers.

filename: components/SignInWithPasskeyButton.tsx
```tsx
export function SignInWithPasskeyButton() {
  const { signIn } = useSignIn()
  const router = useRouter()

  const signInWithPasskey = async () => {
    // 'discoverable' lets the user choose a passkey
    // without auto-filling any of the options
    try {
      const signInAttempt = await signIn?.authenticateWithPasskey({
        flow: 'discoverable',
      })

      if (signInAttempt?.status === 'complete') {
        await setActive({
          session: signInAttempt.createdSessionId,
          redirectUrl: '/',
          navigate: async ({ session, decorateUrl }) => {
            if (session?.currentTask) {
              // Handle pending session tasks
              // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
              console.log(session?.currentTask)
              return
            }

            const url = decorateUrl('/')
            if (url.startsWith('http')) {
              window.location.href = url
            } else {
              router.push(url)
            }
          },
        })
      } else {
        // If the status is not complete, check why. User may need to
        // complete further steps.
        console.error(JSON.stringify(signInAttempt, null, 2))
      }
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
    }
  }

  return <button onClick={signInWithPasskey}>Sign in with a passkey</button>
}
```

## Rename user passkeys

Clerk generates a name based on the device associated with the passkey when it's created. Sometimes users may want to rename a passkey to make it easier to identify.

filename: components/RenamePasskeyUI.tsx
```tsx
export function RenamePasskeyUI() {
  const { user } = useUser()
  const { passkeys } = user

  const passkeyToUpdateId = useRef<HTMLInputElement>(null)
  const newPasskeyName = useRef<HTMLInputElement>(null)
  const [success, setSuccess] = useState(false)

  const renamePasskey = async () => {
    try {
      const passkeyToUpdate = passkeys?.find(
        (pk: PasskeyResource) => pk.id === passkeyToUpdateId.current?.value,
      )

      await passkeyToUpdate?.update({
        name: newPasskeyName.current?.value,
      })

      setSuccess(true)
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
      setSuccess(false)
    }
  }

  return (
    <>
      <p>Passkeys:</p>
      <ul>
        {passkeys?.map((pk: PasskeyResource) => {
          return (
            <li key={pk.id}>
              Name: {pk.name} | ID: {pk.id}
            </li>
          )
        })}
      </ul>
      <input ref={passkeyToUpdateId} type="text" placeholder="Enter the passkey ID" />
      <input type="text" placeholder="Enter the passkey's new name" ref={newPasskeyName} />
      <button onClick={renamePasskey}>Rename passkey</button>
      <p>Passkey updated: {success ? 'Yes' : 'No'}</p>
    </>
  )
}
```

## Delete user passkeys

To delete a user's passkey from your Clerk app, you must call the [delete()](https://clerk.com/docs/reference/types/passkey-resource.md#delete) method of the passkey object, as shown in the following example:

filename: components/DeletePasskeyUI.tsx
```tsx
export function DeletePasskeyUI() {
  const { user } = useUser()
  const { passkeys } = user

  const passkeyToDeleteId = useRef<HTMLInputElement>(null)
  const [success, setSuccess] = useState(false)

  const deletePasskey = async () => {
    const passkeyToDelete = passkeys?.find((pk: any) => pk.id === passkeyToDeleteId.current?.value)
    try {
      await passkeyToDelete?.delete()

      setSuccess(true)
    } catch (err) {
      // See https://clerk.com/docs/guides/development/custom-flows/error-handling
      // for more info on error handling
      console.error('Error:', JSON.stringify(err, null, 2))
      setSuccess(false)
    }
  }

  return (
    <>
      <p>Passkeys:</p>
      <ul>
        {passkeys?.map((pk: any) => {
          return (
            <li key={pk.id}>
              Name: {pk.name} | ID: {pk.id}
            </li>
          )
        })}
      </ul>
      <input ref={passkeyToDeleteId} type="text" placeholder="Enter the passkey ID" />
      <button onClick={deletePasskey}>Delete passkey</button>
      <p>Passkey deleted: {success ? 'Yes' : 'No'}</p>
    </>
  )
}
```

---

## Sitemap

[Overview of all docs pages](https://clerk.com/docs/llms.txt)
