> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rhinestone.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Passkeys

> Control a smart account with WebAuthn credentials and device biometrics.

export const VmCodeGroup = ({children, caption, label = "Virtual machine"}) => {
  const [vm, setVm] = useState("evm");
  const [open, setOpen] = useState(false);
  const selectorRef = useRef(null);
  const triggerRef = useRef(null);
  const menuRef = useRef(null);
  const focusIndex = useRef(0);
  const options = [{
    value: "evm",
    title: "EVM",
    icon: "/images/networks/ethereum.svg"
  }, {
    value: "solana",
    title: "Solana",
    icon: "/images/networks/solana.svg"
  }];
  if (typeof caption !== "string" || caption.trim() === "") {
    throw new Error("VmCodeGroup requires a non-empty caption.");
  }
  const selected = options.find(option => option.value === vm);
  const blocks = (Array.isArray(children) ? children : [children]).filter(child => typeof child !== "string" || child.trim() !== "");
  if (blocks.length !== 2 || !blocks.every(block => typeof block?.props?.language === "string" && block.props.numberOfLines !== undefined) || !options.every(option => blocks.filter(block => block.props.filename === option.title).length === 1)) {
    throw new Error("VmCodeGroup requires two fenced code blocks titled EVM and Solana, with no other content.");
  }
  useEffect(() => {
    const path = window.location.pathname;
    const selection = window.__rhinestoneDocsVm;
    if (selection?.path === path) setVm(selection.vm);
    const onChange = event => {
      if (event.detail.path === path) {
        setVm(event.detail.vm);
        setOpen(false);
      }
    };
    window.addEventListener("rhinestone-docs-vm", onChange);
    return () => window.removeEventListener("rhinestone-docs-vm", onChange);
  }, []);
  useEffect(() => {
    if (!open) return;
    menuRef.current?.querySelectorAll("button")[focusIndex.current]?.focus();
    const dismiss = event => {
      if (!selectorRef.current?.contains(event.target)) setOpen(false);
    };
    document.addEventListener("pointerdown", dismiss);
    return () => document.removeEventListener("pointerdown", dismiss);
  }, [open]);
  const selectVm = value => {
    const selection = {
      path: window.location.pathname,
      vm: value
    };
    window.__rhinestoneDocsVm = selection;
    window.dispatchEvent(new CustomEvent("rhinestone-docs-vm", {
      detail: selection
    }));
    triggerRef.current?.focus();
  };
  const openMenu = index => {
    focusIndex.current = index;
    setOpen(true);
    menuRef.current?.querySelectorAll("button")[index]?.focus();
  };
  const handleMenuKey = event => {
    const buttons = Array.from(menuRef.current.querySelectorAll("button"));
    const current = buttons.indexOf(document.activeElement);
    let next;
    if (event.key === "ArrowDown") next = (current + 1) % buttons.length;
    if (event.key === "ArrowUp") next = (current + buttons.length - 1) % buttons.length;
    if (event.key === "Home" || event.key.toLowerCase() === "e") next = 0;
    if (event.key === "End" || event.key.toLowerCase() === "s") next = buttons.length - 1;
    if (next !== undefined) {
      event.preventDefault();
      buttons[next].focus();
    }
    if (event.key === "Tab") setOpen(false);
  };
  return <div className="vm-code-group">
      <div className="vm-code-group-header">
        <span className="vm-code-caption" title={caption}>
          {caption}
        </span>
        <div className="vm-code-selector" ref={selectorRef} onBlur={event => {
    if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
  }} onKeyDown={event => {
    if (event.key === "Escape" && open) {
      event.preventDefault();
      setOpen(false);
      triggerRef.current?.focus();
    }
  }}>
          <button type="button" className="vm-code-trigger" ref={triggerRef} aria-label={`${label}: ${selected.title}`} aria-haspopup="menu" aria-expanded={open} onClick={() => open ? setOpen(false) : openMenu(options.indexOf(selected))} onKeyDown={event => {
    if (event.key === "ArrowDown" || event.key === "ArrowUp") {
      event.preventDefault();
      openMenu(event.key === "ArrowDown" ? 0 : options.length - 1);
    }
  }}>
            <img className="vm-code-icon" src={selected.icon} alt="" width="16" height="16" />
            <span>{selected.title}</span>
            <svg className="vm-code-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <path d="m7 10 5 5 5-5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
          </button>
          {open && <div className="vm-code-menu" role="menu" aria-label={label} ref={menuRef} onKeyDown={handleMenuKey}>
              {options.map(option => <button type="button" role="menuitemradio" aria-checked={vm === option.value} tabIndex={-1} className="vm-code-option" key={option.value} onClick={() => selectVm(option.value)}>
                  <img className="vm-code-icon" src={option.icon} alt="" width="16" height="16" />
                  <span>{option.title}</span>
                  {vm === option.value && <svg className="vm-code-check" width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                      <path d="m5 12 4 4L19 6" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
                    </svg>}
                </button>)}
            </div>}
        </div>
      </div>
      {options.map(option => {
    const block = blocks.find(block => block.props.filename === option.title);
    const CodeBlock = block.type;
    return <div key={option.value} className="vm-code-group-panel" data-vm={option.value} hidden={option.value !== vm}>
            <CodeBlock {...block.props} filename={undefined} />
          </div>;
  })}
    </div>;
};

Choose passkeys for consumer apps that should use Face ID, Touch ID, Windows Hello, or another WebAuthn authenticator instead of a seed phrase. The authenticator keeps the private key on the user's device.

## Create a passkey owner

Create the WebAuthn credential in a browser, convert it to a Viem account, and pass it as the owner:

```ts theme={null}
import {
  createWebAuthnCredential,
  toWebAuthnAccount,
} from 'viem/account-abstraction'

const credential = await createWebAuthnCredential({
  name: 'Your app',
})
const passkeyOwner = toWebAuthnAccount({ credential })

```

<VmCodeGroup caption="Configure a passkey owner">
  ```ts EVM theme={null}
  const account = await rhinestone.createAccount({
    evm: {
      owners: {
        type: 'passkey',
        accounts: [passkeyOwner],
      },
    },
  })
  ```

  ```ts Solana theme={null}
  import { createSolanaSwigId } from '@rhinestone/sdk/solana'

  // Persist id and swig with the credential metadata.
  // Deploy before funding; the initial root authority is permanent.
  const { id, swig } = createSolanaSwigId()
  const account = await rhinestone.createAccount({
    solana: { swig, owner: { type: 'passkey', account: passkeyOwner } },
  })
  ```
</VmCodeGroup>

Save the account configuration with the credential metadata. Follow [account configuration](/wallets/custom-signer/account-setup/create-an-account) for deployment and restoration before funding.

Here, `rhinestone` is a configured `RhinestoneSDK` instance. Do not put an API key in browser code; route SDK requests through your backend or use an appropriate client authentication flow.

Store `credential.id` and `credential.publicKey` so you can reconstruct the Viem account with `toWebAuthnAccount`. Creating another credential changes the smart account configuration and can change its address.

<Note>
  Passkey availability and synchronization depend on the browser, operating system, authenticator, and relying-party domain. Plan a recovery path instead of assuming a passkey will sync to every device.
</Note>

## Use more than one passkey

For EVM, use [multisig](/wallets/custom-signer/account-setup/native-signers/multisig) for a passkey-per-device setup or when several passkeys must approve a transaction. Use [multi-factor authentication](/wallets/custom-signer/account-setup/native-signers/multi-factor-authentication) to combine a passkey with another validator type.

## Manage EVM passkeys

Use the SDK actions to change an account after deployment:

* [`addOwner`](/wallets/custom-signer/sdk-reference/actions/passkeys/add-owner)
* [`removeOwner`](/wallets/custom-signer/sdk-reference/actions/passkeys/remove-owner)
* [`changeThreshold`](/wallets/custom-signer/sdk-reference/actions/passkeys/change-threshold)

## Manage Solana passkeys

Continue the Solana setup above with `additionalPasskey`, created or restored using the shared WebAuthn procedure. Add or remove one non-root authority per transaction:

```ts theme={null}
import { addPasskey, removePasskey, solanaDevnet } from '@rhinestone/sdk/solana'

const addAuthority = {
  destination: {
    chain: solanaDevnet,
    authority: addPasskey(additionalPasskey, { permission: 'allButManageAuthority' }),
  },
}
const removeAuthority = {
  destination: {
    chain: solanaDevnet,
    authority: removePasskey(additionalPasskey),
  },
}
```

Use `allButManageAuthority` for spending and instructions without authority management. Use `all` for both spending and management, or `manageAuthority` for management without spending. Either management permission can grant `all` to another key, so both confer takeover power.

The configured signer must hold `all` or `manageAuthority` to submit changes through the [shared transaction lifecycle](/transactions/multichain/send-a-transaction). Changes are always sponsored, including new-role rent; omit `source` and `sponsored`. Settle an existing submission before retrying; `getAuthorityStatus(addAuthority)` must return `applied`, not `conflict`, to confirm the exact permission.

The root cannot be removed, nor can the last management role. Adding another passkey creates a role, not an EVM threshold validator or MFA policy. See [Solana support](/wallets/custom-signer/solana-support).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.