Documentation Index
Fetch the complete documentation index at: https://docs.chipipay.com/llms.txt
Use this file to discover all available pages before exploring further.
Why Recovery Matters
Passkeys can become unavailable:
- User switches browsers (Chrome → Firefox)
- Browser update breaks PRF extension
- Device reset clears WebAuthn credentials
- User gets a new device
The dual-key architecture ensures the wallet is always recoverable via PIN.
PIN Backup Flow
When a passkey fails, use the PIN to decrypt:
import { verifyWalletPasskeyDetailed } from "@chipi-stack/chipi-passkey";
// 1. Try passkey first
const result = await verifyWalletPasskeyDetailed();
if (result.valid) {
// Passkey works — use it normally
const encryptKey = await getWalletEncryptKey();
// decrypt wallet with encryptKey
} else if (result.reason === "prf_unavailable") {
// Passkey exists but PRF failed (browser change)
// Fall back to PIN
promptUserForPin();
} else if (result.reason === "no_passkey") {
// No passkey stored — user needs to enter PIN
promptUserForPin();
}
Detecting PRF Unavailability
const result = await verifyWalletPasskeyDetailed();
switch (result.reason) {
case undefined:
// Valid — passkey works
break;
case "no_passkey":
// No credential in localStorage
// User needs PIN or to re-register passkey
break;
case "prf_unavailable":
// Credential exists but browser can't derive PRF key
// DO NOT fall back to PBKDF2 — it produces a different key
// Use PIN instead
break;
case "error":
// Other error (network, timeout, etc.)
break;
}
When PRF is unavailable for a credential that was created WITH PRF, the SDK throws instead of silently falling back to PBKDF2. This is intentional — PBKDF2 produces a different key and would fail to decrypt the wallet. Always use PIN backup in this case.
Migration: PIN-Only → Passkey
For wallets created before passkeys were available:
import { useMigrateWalletToPasskey } from "@chipi-stack/nextjs";
function MigrateToPasskey({ wallet, userId }: { wallet: WalletData; userId: string }) {
const { migrateWalletToPasskeyAsync, isLoading } = useMigrateWalletToPasskey();
const [pin, setPin] = useState("");
const handleMigrate = async () => {
const bearerToken = await getToken();
// User enters their existing PIN to decrypt
// Then biometric prompt creates the passkey
const result = await migrateWalletToPasskeyAsync({
wallet, // full WalletData object
oldEncryptKey: pin, // current PIN
externalUserId: userId, // your user ID
bearerToken,
});
// result.success === true
// result.credentialId — the new passkey credential
// result.wallet — updated wallet with authMethod: "passkey+pin"
};
return (
<button onClick={handleMigrate} disabled={isLoading || !pin}>
Upgrade to Passkey
</button>
);
}
Credential Storage
Passkey metadata is stored in localStorage under the key chipi_wallet_passkey_credential:
interface WalletCredentialMetadata {
credentialId: string; // WebAuthn credential ID
createdAt: string; // ISO timestamp
userId: string; // Your external user ID
transports?: AuthenticatorTransport[]; // ["internal", "hybrid"] etc.
prfSupported?: boolean; // Whether PRF worked at creation
}
Important: prfSupported determines whether to attempt PRF on future auth. If true and PRF fails later (browser change), the SDK throws instead of silently using PBKDF2.
Clearing credentials
import { removeStoredCredential } from "@chipi-stack/chipi-passkey";
// On logout or account reset
removeStoredCredential();
// Clears localStorage — user will need PIN on next login
Best Practices
- Always require a PIN backup — never create a passkey-only wallet
- Check
verifyWalletPasskeyDetailed() before transactions — detect failures early
- Show clear UI when passkey fails — “Your biometric isn’t working. Enter your PIN instead.”
- Don’t auto-retry passkey on failure — if the browser can’t do PRF, retrying won’t help
- Store
prfSupported with the wallet — send it to your backend for conditional fallback logic