Skip to main content

What Are Passkeys?

Passkeys replace PINs with biometric authentication (Face ID, Touch ID, Windows Hello). Instead of asking users to create and remember a PIN to encrypt their wallet, passkeys generate a secure encryption key gated behind biometric auth. No passwords to remember, no PINs to leak.

How It Works

PIN flow:    User enters PIN → PIN encrypts private key → stored in wallet
Passkey flow: User taps fingerprint → random key generated → key encrypts private key → stored in wallet
The encryption key is derived on-demand from the biometric prompt. It is never stored in plaintext.

Platform Support

PlatformTechnologyPackage
Web (Next.js, React)WebAuthn API@chipi-stack/chipi-passkey
Mobile (Expo)expo-local-authentication + expo-secure-store@chipi-stack/chipi-expo

Browser Compatibility

  • Chrome 67+ (Desktop & Android)
  • Safari 14+ (iOS 14+, macOS)
  • Firefox 60+ (Desktop)
  • Edge 18+ (Desktop)

Installation

npm install @chipi-stack/chipi-passkey

Hooks Reference

HookPurpose
usePasskeySetupCreate a new passkey (triggers biometric prompt, returns encryption key)
usePasskeyAuthAuthenticate with existing passkey (returns encryption key)
usePasskeyStatusCheck WebAuthn/biometric support and stored passkey validity
useMigrateWalletToPasskeyMigrate from PIN-based wallet to passkey

Create a Wallet with Passkey

"use client";

import { useCreateWallet } from "@chipi-stack/nextjs";
import { usePasskeySetup } from "@chipi-stack/chipi-passkey/hooks";

export function CreateWalletWithPasskey() {
  const { createWalletAsync, isLoading } = useCreateWallet();
  const { setupPasskey } = usePasskeySetup();

  const handleCreate = async () => {
    // Triggers biometric prompt → returns random encryption key
    const encryptKey = await setupPasskey();

    const result = await createWalletAsync({
      params: {
        encryptKey,
        usePasskey: true,
        externalUserId: "user-123",
      },
      bearerToken: await getBearerToken(),
    });

    localStorage.setItem("wallet", JSON.stringify(result.wallet));
  };

  return (
    <button onClick={handleCreate} disabled={isLoading}>
      {isLoading ? "Creating..." : "Create Wallet with Passkey"}
    </button>
  );
}

Transfer with Passkey

"use client";

import { useTransfer } from "@chipi-stack/nextjs";
import { usePasskeyAuth } from "@chipi-stack/chipi-passkey/hooks";

export function TransferWithPasskey() {
  const { transferAsync, isLoading } = useTransfer();
  const { authenticatePasskey } = usePasskeyAuth();

  const handleTransfer = async (recipient: string, amount: string) => {
    // Triggers biometric prompt → returns the encryption key
    const encryptKey = await authenticatePasskey();
    const wallet = JSON.parse(localStorage.getItem("wallet")!);

    const txHash = await transferAsync({
      params: {
        encryptKey,
        usePasskey: true,
        wallet,
        token: "USDC",
        recipient,
        amount: Number(amount),
      },
      bearerToken: await getBearerToken(),
    });
  };
}

Migrate from PIN to Passkey

For users who already have a PIN-based wallet:
"use client";

import { useMigrateWalletToPasskey } from "@chipi-stack/chipi-passkey/hooks";

export function MigrateToPasskey() {
  const { migrateWalletToPasskeyAsync, isLoading } = useMigrateWalletToPasskey();

  const handleMigrate = async (currentPin: string) => {
    const wallet = JSON.parse(localStorage.getItem("wallet")!);

    // Flow: decrypt with old PIN → create passkey → re-encrypt with passkey key → update wallet
    await migrateWalletToPasskeyAsync({
      wallet,
      oldEncryptKey: currentPin,
      externalUserId: "user-123",
      bearerToken: await getBearerToken(),
    });

    // Wallet is now passkey-protected. PIN no longer works.
  };

  return (
    <div>
      <input type="password" placeholder="Current PIN" id="pin" />
      <button onClick={() => handleMigrate((document.getElementById("pin") as HTMLInputElement).value)} disabled={isLoading}>
        {isLoading ? "Migrating..." : "Switch to Passkey"}
      </button>
    </div>
  );
}

Check Passkey Support

import { usePasskeyStatus } from "@chipi-stack/chipi-passkey/hooks";

function PasskeyGate({ children }) {
  const { isSupported, hasPasskey } = usePasskeyStatus();

  if (!isSupported) {
    return <p>Your browser doesn't support passkeys. Please use a PIN instead.</p>;
  }

  if (!hasPasskey) {
    return <SetupPasskeyPrompt />;
  }

  return children;
}

Expo (Mobile) Passkeys

On mobile, passkeys use the device’s biometric hardware (Face ID, Touch ID) via expo-local-authentication and store the encryption key in expo-secure-store.
import { ChipiProvider } from "@chipi-stack/chipi-expo";
import {
  isNativeBiometricSupported,
  createNativeWalletPasskey,
  getNativeWalletEncryptKey,
  hasNativeWalletPasskey,
} from "@chipi-stack/chipi-expo";

// Check support
const supported = await isNativeBiometricSupported();

// Create passkey (triggers Face ID / Touch ID)
const encryptKey = await createNativeWalletPasskey();

// Authenticate later (triggers biometric again)
const key = await getNativeWalletEncryptKey();

// Check if passkey exists
const exists = await hasNativeWalletPasskey();

Device Requirements

  • iOS: Face ID or Touch ID capable device, iOS 14+
  • Android: Device with fingerprint sensor or face unlock, API 23+

Security Benefits

  • No PINs stored - Encryption key derived on-demand from biometric
  • Hardware-backed - Keys stored in Secure Enclave (iOS) or Android Keystore
  • Phishing resistant - WebAuthn credentials are domain-bound
  • Synced across devices - Via iCloud Keychain or Google Password Manager (web only)
Need help? Join our Telegram Community for support.