Overview
WebAuthn passkeys replace PINs with biometric authentication (Face ID, Touch ID, Windows Hello). No PINs to remember or store.
Prerequisites
- Next.js 13+ with App Router
- Modern browser with WebAuthn support
- Biometric hardware (fingerprint, face recognition)
- Chipi Next.js SDK installed
Installation
npm install @chipi-stack/chipi-passkey
Basic Implementation
Create wallet with passkey
"use client";
import { usePasskeySetup } from "@chipi-stack/nextjs";
Authenticate with passkey
"use client";
import { usePasskeyAuth } from "@chipi-stack/nextjs";
Migrate existing wallet (optional)
If users already have a PIN-based wallet, they can migrate to passkey:"use client";
import { useMigrateWalletToPasskey } from "@chipi-stack/nextjs";
Example
Create Wallet
Transfer with Passkey
Migrate to Passkey
"use client";
import { useCreateWallet } from '@chipi-stack/nextjs';
import { usePasskeySetup } from '@chipi-stack/nextjs';
import { useState } from 'react';
export function CreateWalletWithPasskey() {
const { createWalletAsync, isLoading, error } = useCreateWallet();
const { setupPasskey } = usePasskeySetup();
const [wallet, setWallet] = useState(null);
const handleCreate = async () => {
try {
// Triggers biometric prompt
const encryptKey = await setupPasskey();
const result = await createWalletAsync({
params: {
encryptKey,
usePasskey: true,
externalUserId: 'user-123',
},
bearerToken: 'your-jwt-token',
});
localStorage.setItem('wallet', JSON.stringify(result.wallet));
setWallet(result);
} catch (err) {
console.error(err);
}
};
return (
<div className="p-6 bg-white rounded-lg shadow">
<button
onClick={handleCreate}
disabled={isLoading}
className="px-4 py-2 bg-green-600 text-white rounded disabled:bg-gray-400"
>
{isLoading ? 'Creating...' : 'Create Wallet with Passkey'}
</button>
{error && <p className="text-red-600 mt-2">Error: {error.message}</p>}
{wallet && (
<p className="text-sm mt-4 font-mono">
Wallet created: {wallet.wallet.publicKey}
</p>
)}
</div>
);
}
"use client";
import { useTransfer } from '@chipi-stack/nextjs';
import { usePasskeyAuth } from '@chipi-stack/nextjs';
import { useState } from 'react';
export function TransferWithPasskey() {
const { transferAsync, isLoading } = useTransfer();
const { authenticatePasskey } = usePasskeyAuth();
const [recipient, setRecipient] = useState('');
const [amount, setAmount] = useState('');
const handleTransfer = async () => {
try {
// Triggers biometric prompt
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: 'your-jwt-token',
});
alert(`Transfer complete: ${txHash}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="space-y-4 p-6 bg-white rounded-lg shadow">
<input
placeholder="Recipient address"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
className="w-full p-2 border rounded"
/>
<input
placeholder="Amount"
value={amount}
onChange={(e) => setAmount(e.target.value)}
type="number"
className="w-full p-2 border rounded"
/>
<button
onClick={handleTransfer}
disabled={isLoading}
className="w-full px-4 py-2 bg-green-600 text-white rounded disabled:bg-gray-400"
>
{isLoading ? 'Sending...' : 'Send with Passkey'}
</button>
</div>
);
}
"use client";
import { useMigrateWalletToPasskey } from '@chipi-stack/nextjs';
import { usePasskeySetup } from '@chipi-stack/nextjs';
import { useState } from 'react';
export function MigrateToPasskey() {
const { migrateWalletToPasskeyAsync, isLoading } = useMigrateWalletToPasskey();
const { setupPasskey } = usePasskeySetup();
const [currentPin, setCurrentPin] = useState('');
const handleMigrate = async () => {
try {
const newEncryptKey = await setupPasskey();
const wallet = JSON.parse(localStorage.getItem('wallet')!);
await migrateWalletToPasskeyAsync({
params: {
oldEncryptKey: currentPin,
newEncryptKey,
wallet,
},
bearerToken: 'your-jwt-token',
});
alert('Successfully migrated to passkey!');
} catch (err) {
console.error(err);
}
};
return (
<div className="space-y-4 p-6 bg-white rounded-lg shadow">
<input
type="password"
placeholder="Current PIN"
value={currentPin}
onChange={(e) => setCurrentPin(e.target.value)}
className="w-full p-2 border rounded"
/>
<button
onClick={handleMigrate}
disabled={isLoading}
className="w-full px-4 py-2 bg-green-600 text-white rounded disabled:bg-gray-400"
>
{isLoading ? 'Migrating...' : 'Migrate to Passkey'}
</button>
</div>
);
}
Hooks Reference
| Hook | Purpose |
|---|
usePasskeySetup | Create new passkey |
usePasskeyAuth | Authenticate with existing passkey |
usePasskeyStatus | Check passkey support & status |
useMigrateWalletToPasskey | Migrate from PIN to passkey |
Browser Support
- ✅ Chrome 67+ (Desktop & Android)
- ✅ Safari 14+ (iOS 14+, macOS)
- ✅ Firefox 60+ (Desktop)
- ✅ Edge 18+ (Desktop)
Passkeys are stored in the browser and synced via platform (iCloud Keychain, Google Password Manager).
Security Benefits
- No PINs stored - Keys derived on-demand from biometric
- Platform-level security - Hardware-backed authentication
- Phishing resistant - Domain-bound credentials
- User convenience - One tap authentication
Next Steps